-
Notifications
You must be signed in to change notification settings - Fork 467
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sdk-analytics) fix comments, simplify lib
- Loading branch information
Showing
12 changed files
with
245 additions
and
345 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 59 additions & 82 deletions
141
core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
159 changes: 27 additions & 132 deletions
159
core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.