Skip to content

Commit

Permalink
feat(sdk-analytics) fix comments, simplify lib
Browse files Browse the repository at this point in the history
  • Loading branch information
oidacra committed Dec 19, 2024
1 parent d097815 commit b8b679d
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 345 deletions.
31 changes: 31 additions & 0 deletions core-web/libs/sdk/analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@ function Activity({ title, urlTitle }) {
}
```

### Manual Page View Tracking

To manually track page views, first disable automatic tracking in your config:

```tsx
const analyticsConfig = {
apiKey: 'your-api-key-from-dotcms-analytics-app',
server: 'https://your-dotcms-instance.com',
autoPageView: false // Disable automatic tracking
};
```

Then use the `useContentAnalytics` hook in your layout component:

```tsx
import { useContentAnalytics } from '@dotcms/analytics/react';

function Layout({ children }) {
const { pageView } = useContentAnalytics();

useEffect(() => {
pageView({
// Add any custom properties you want to track
myCustomValue: '2'
});
}, []);

return <div>{children}</div>;
}
```

## Browser Configuration

The script can be configured using data attributes:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,110 +1,87 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import Analytics from 'analytics';

import { DotContentAnalytics } from './dot-content-analytics';
import { dotAnalytics } from './plugin/dot-analytics.plugin';
import { initializeContentAnalytics } from './dot-content-analytics';
import { DotContentAnalyticsConfig } from './shared/dot-content-analytics.model';
import { createAnalyticsInstance } from './shared/dot-content-analytics.utils';

// Mock the analytics library
jest.mock('analytics');
jest.mock('./plugin/dot-analytics.plugin');
// Mock dependencies
jest.mock('./shared/dot-content-analytics.utils');

describe('DotAnalytics', () => {
describe('initializeContentAnalytics', () => {
const mockConfig: DotContentAnalyticsConfig = {
debug: false,
server: 'http://test.com',
apiKey: 'test-key',
autoPageView: false
apiKey: 'test-key'
};

const mockAnalyticsInstance = {
page: jest.fn(),
track: jest.fn()
};

beforeEach(() => {
jest.clearAllMocks();
// Reset singleton instance between tests
(DotContentAnalytics as any).instance = null;
(createAnalyticsInstance as jest.Mock).mockReturnValue(mockAnalyticsInstance);
});

describe('getInstance', () => {
it('should create single instance', () => {
const instance1 = DotContentAnalytics.getInstance(mockConfig);
const instance2 = DotContentAnalytics.getInstance(mockConfig);
it('should create analytics instance with correct config', () => {
initializeContentAnalytics(mockConfig);
expect(createAnalyticsInstance).toHaveBeenCalledWith(mockConfig);
});

describe('pageView', () => {
it('should call analytics.page with provided payload', () => {
const payload = { path: '/test' };
const analytics = initializeContentAnalytics(mockConfig);

expect(instance1).toBe(instance2);
analytics.pageView(payload);

expect(mockAnalyticsInstance.page).toHaveBeenCalledWith(payload);
});

it('should maintain same instance even with different config', () => {
const instance1 = DotContentAnalytics.getInstance(mockConfig);
const instance2 = DotContentAnalytics.getInstance({ ...mockConfig, debug: true });
it('should call analytics.page with empty object when no payload provided', () => {
const analytics = initializeContentAnalytics(mockConfig);

analytics.pageView();

expect(instance1).toBe(instance2);
expect(mockAnalyticsInstance.page).toHaveBeenCalledWith({});
});
});

describe('ready', () => {
it('should initialize analytics with correct config', async () => {
const instance = DotContentAnalytics.getInstance(mockConfig);
const mockAnalytics = {};

(Analytics as jest.Mock).mockReturnValue(mockAnalytics);
(dotAnalytics as jest.Mock).mockReturnValue({ name: 'mock-plugin' });

// Mock del enricher plugin
jest.mock('./plugin/dot-analytics.enricher.plugin', () => ({
dotAnalyticsEnricherPlugin: {
name: 'enrich-dot-analytics',
'page:dot-analytics': jest.fn(),
'track:dot-analytics': jest.fn()
}
}));

await instance.ready();

expect(Analytics).toHaveBeenCalledWith({
app: 'dotAnalytics',
debug: false,
plugins: [
{
name: 'enrich-dot-analytics',
'page:dot-analytics': expect.any(Function),
'track:dot-analytics': expect.any(Function)
},
{ name: 'mock-plugin' }
]
});
expect(dotAnalytics).toHaveBeenCalledWith(mockConfig);
describe('track', () => {
it('should call analytics.track with event name and payload', () => {
const eventName = 'test-event';
const payload = { value: 123 };
const analytics = initializeContentAnalytics(mockConfig);

analytics.track(eventName, payload);

expect(mockAnalyticsInstance.track).toHaveBeenCalledWith(eventName, payload);
});

it('should only initialize once', async () => {
const instance = DotContentAnalytics.getInstance(mockConfig);
it('should call analytics.track with empty object when no payload provided', () => {
const eventName = 'test-event';
const analytics = initializeContentAnalytics(mockConfig);

await instance.ready();
await instance.ready();
analytics.track(eventName);

expect(mockAnalyticsInstance.track).toHaveBeenCalledWith(eventName, {});
});
});

describe('when analytics instance is null', () => {
beforeEach(() => {
(createAnalyticsInstance as jest.Mock).mockReturnValue(null);
});

expect(Analytics).toHaveBeenCalledTimes(1);
it('should handle null analytics instance for pageView', () => {
const analytics = initializeContentAnalytics(mockConfig);
analytics.pageView({ path: '/test' });
expect(mockAnalyticsInstance.page).not.toHaveBeenCalled();
});

it('should throw error if initialization fails', async () => {
const instance = DotContentAnalytics.getInstance(mockConfig);
const error = new Error('Init failed');
(Analytics as jest.Mock).mockImplementation(() => {
throw error;
});

const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {
// Do nothing
});

try {
await instance.ready();
fail('Should have thrown an error');
} catch (e) {
expect(e).toEqual(error);
expect(console.error).toHaveBeenCalledWith(
'[dotCMS DotContentAnalytics] Failed to initialize: Error: Init failed'
);
}

consoleErrorMock.mockRestore();
it('should handle null analytics instance for track', () => {
const analytics = initializeContentAnalytics(mockConfig);
analytics.track('test-event', { value: 123 });
expect(mockAnalyticsInstance.track).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,136 +1,31 @@
import Analytics, { AnalyticsInstance } from 'analytics';

import { dotAnalyticsEnricherPlugin } from './plugin/dot-analytics.enricher.plugin';
import { dotAnalytics } from './plugin/dot-analytics.plugin';
import { DotContentAnalyticsConfig } from './shared/dot-content-analytics.model';
import { DotLogger } from './utils/DotLogger';
import { DotAnalytics, DotContentAnalyticsConfig } from './shared/dot-content-analytics.model';
import { createAnalyticsInstance } from './shared/dot-content-analytics.utils';

/**
* DotContentAnalytics class for sending events to Content Analytics.
* This class handles tracking events and automatically collects browser information
* like user agent, viewport size, and other relevant browser metadata to provide
* better analytics insights.
* Creates an analytics instance.
*
* The class follows a singleton pattern to ensure only one analytics instance
* is running at a time. It can be initialized with configuration options including
* server URL, debug mode, auto page view tracking, and API key.
* @param {DotContentAnalyticsConfig} config - The configuration object for the analytics instance.
* @returns {DotAnalytics} - The analytics instance.
*/
export class DotContentAnalytics {
private static instance: DotContentAnalytics | null = null;
private readonly logger: DotLogger;
#initialized = false;
#analytics: AnalyticsInstance | null = null;
#config: DotContentAnalyticsConfig;

private constructor(config: DotContentAnalyticsConfig) {
this.#config = config;
this.logger = new DotLogger(config.debug, 'DotContentAnalytics');

if (!config.apiKey) {
this.#initialized = false;
}
}

/**
* Returns the singleton instance of DotContentAnalytics
*/
static getInstance(config: DotContentAnalyticsConfig): DotContentAnalytics {
if (!config.apiKey) {
console.error(
`DotContentAnalytics: Missing "apiKey" in configuration - Events will not be sent to Content Analytics`
);
}

if (!config.server) {
console.error(
`DotContentAnalytics: Missing "server" in configuration - Events will not be sent to Content Analytics`
);
}

if (!DotContentAnalytics.instance) {
DotContentAnalytics.instance = new DotContentAnalytics(config);
}

return DotContentAnalytics.instance;
}

/**
* Initializes the analytics instance
*/
async ready(): Promise<void> {
if (this.#initialized) {
this.logger.log('Already initialized');

return Promise.resolve();
}

try {
this.logger.group('Initialization');
this.logger.time('Init');

const plugins = this.#getPlugins();

this.#analytics = Analytics({
app: 'dotAnalytics',
debug: this.#config.debug,
plugins
});

this.#initialized = true;
this.logger.log('dotAnalytics initialized');
this.logger.timeEnd('Init');
this.logger.groupEnd();

return Promise.resolve();
} catch (error) {
this.logger.error(`Failed to initialize: ${error}`);
throw error;
}
}

/**
* Returns the plugins to be used in the analytics instance
*/
#getPlugins() {
const hasRequiredConfig = this.#config.apiKey && this.#config.server;

if (!hasRequiredConfig) {
return [];
}

return [dotAnalyticsEnricherPlugin, dotAnalytics(this.#config)];
}

/**
* Sends a page view event to the analytics instance.
*
* @param {Record<string, unknown>} payload - The payload to send to the analytics instance.
* @returns {void}
*/
pageView(payload: Record<string, unknown> = {}): void {
if (!this.#analytics || !this.#initialized) {
this.logger.warn('Not initialized');

return;
}

this.#analytics.page(payload);
}

/**
* Sends a track event to the analytics instance.
*
* @param {string} eventName - The name of the event to send.
* @param {Record<string, unknown>} payload - The payload to send to the analytics instance.
* @returns {void}
*/
track(eventName: string, payload: Record<string, unknown> = {}): void {
if (!this.#analytics || !this.#initialized) {
this.logger.warn('Not initialized');

return;
}

this.#analytics.track(eventName, payload);
}
}
export const initializeContentAnalytics = (config: DotContentAnalyticsConfig): DotAnalytics => {
const analytics = createAnalyticsInstance(config);

return {
/**
* Track a page view.
* @param {Record<string, unknown>} payload - The payload to track.
*/
pageView: (payload: Record<string, unknown> = {}) => {
analytics?.page(payload);
},

/**
* Track a custom event.
* @param {string} eventName - The name of the event to track.
* @param {Record<string, unknown>} payload - The payload to track.
*/
track: (eventName: string, payload: Record<string, unknown> = {}) => {
analytics?.track(eventName, payload);
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,11 @@ export interface DotAnalyticsParams {
config: DotContentAnalyticsConfig;
payload: DotAnalyticsPayload;
}

/**
* Shared interface for the DotAnalytics plugin
*/
export interface DotAnalytics {
pageView: (payload?: Record<string, unknown>) => void;
track: (eventName: string, payload?: Record<string, unknown>) => void;
}
Loading

0 comments on commit b8b679d

Please sign in to comment.