diff --git a/libraries/tracker-core/src/core.ts b/libraries/tracker-core/src/core.ts index 0dfff0d59..cbe78a044 100644 --- a/libraries/tracker-core/src/core.ts +++ b/libraries/tracker-core/src/core.ts @@ -157,6 +157,13 @@ export interface TrackerCore { */ addPayloadPair: (key: string, value: unknown) => void; + /** + * Deactivate tracker core including all plugins. + * This is useful for cleaning up resources or listeners that have been created. + * Once deactivated, the tracker won't be able to track any events. + */ + deactivate(): void; + /** * Get current base64 encoding state */ @@ -387,6 +394,11 @@ export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore context?: Array> | null, timestamp?: Timestamp | null ): Payload | undefined { + if (!active) { + LOG.error('Track called on deactivated tracker'); + return undefined; + } + pb.withJsonProcessor(payloadJsonProcessor(encodeBase64)); pb.add('eid', uuid()); pb.addDict(payloadPairs); @@ -539,6 +551,7 @@ export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore return core; } + let active = true; const { base64, corePlugins, callback } = configuration, plugins = corePlugins ?? [], partialCore = newCore(base64 ?? true, plugins, callback), @@ -550,6 +563,13 @@ export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore plugin.logger?.(LOG); plugin.activateCorePlugin?.(core); }, + deactivate: () => { + plugins.forEach((plugin) => { + plugin.deactivatePlugin?.(core); + }); + plugins.length = 0; + active = false; + } }; plugins?.forEach((plugin) => { diff --git a/libraries/tracker-core/src/plugins.ts b/libraries/tracker-core/src/plugins.ts index 4d1f6c2d9..7a8b11b52 100644 --- a/libraries/tracker-core/src/plugins.ts +++ b/libraries/tracker-core/src/plugins.ts @@ -43,6 +43,11 @@ export interface CorePlugin { * Use to capture the specific core instance for each instance of a core plugin */ activateCorePlugin?: (core: TrackerCore) => void; + /** + * Called when the tracker is being destroyed. + * Should be used to clean up any resources or listeners that the plugin has created. + */ + deactivatePlugin?: (core: TrackerCore) => void; /** * Called before the `filter` method is called and before the trackerCore callback fires (if the filter passes) * @param payloadBuilder - The payloadBuilder which will be sent to the callback, can be modified diff --git a/libraries/tracker-core/test/core.ts b/libraries/tracker-core/test/core.ts index aac62a570..226e9dc7e 100644 --- a/libraries/tracker-core/test/core.ts +++ b/libraries/tracker-core/test/core.ts @@ -70,6 +70,10 @@ function compare(result: Payload, expected: Payload, t: ExecutionContext) { t.deepEqual(result, expected); } +test.before(() => { + console.error = () => {}; // Silence console.error globally +}); + test('tracker.track API should return the eid attribute', (t) => { const pageUrl = 'http://www.example.com'; const pageTitle = 'title page'; @@ -1034,3 +1038,47 @@ test('filter is passed full payload including dynamic context', (t) => { t.assert(countTracked === 1); }); + +test('doesnt track any events on deactivated tracker', (t) => { + let countTracked = 0; + const tracker = trackerCore({ + corePlugins: [ + { + afterTrack: () => { + countTracked += 1; + }, + }, + ], + }); + + tracker.deactivate(); + + t.falsy( + tracker.track( + buildPageView({ + pageUrl: 'http://www.example.com', + pageTitle: 'title page', + referrer: 'https://www.google.com', + }) + ) + ); + + t.assert(countTracked === 0); +}); + +test('deactivates plugins on deactivated tracker', (t) => { + let pluginDeactivated = false; + const tracker = trackerCore({ + corePlugins: [ + { + deactivatePlugin: () => { + pluginDeactivated = true; + } + }, + ], + }); + + tracker.deactivate(); + + t.assert(pluginDeactivated); +}); diff --git a/trackers/react-native-tracker/src/tracker.ts b/trackers/react-native-tracker/src/tracker.ts index ac9e87e59..b451947b3 100644 --- a/trackers/react-native-tracker/src/tracker.ts +++ b/trackers/react-native-tracker/src/tracker.ts @@ -1,4 +1,4 @@ -import { trackerCore, PayloadBuilder, version, EmitterConfiguration } from '@snowplow/tracker-core'; +import { trackerCore, PayloadBuilder, version, EmitterConfiguration, TrackerCore } from '@snowplow/tracker-core'; import { newEmitter } from '@snowplow/tracker-core'; import { newReactNativeEventStore } from './event_store'; @@ -13,6 +13,8 @@ import { TrackerConfiguration, } from './types'; +const initializedTrackers: Record = {}; + /** * Creates a new tracker instance with the given configuration * @param configuration - Configuration for the tracker @@ -45,7 +47,7 @@ export async function newTracker( core.setAppId(appId); } - return { + const tracker = { ...newTrackEventFunctions(core), ...subject.properties, setAppId: core.setAppId, @@ -56,4 +58,36 @@ export async function newTracker( clearGlobalContexts: core.clearGlobalContexts, addPlugin: core.addPlugin, }; + initializedTrackers[namespace] = { tracker, core }; + return tracker; +} + +/** + * Retrieves an initialized tracker given its namespace + * @param trackerNamespace Tracker namespace + * @returns Tracker instance if exists + */ +export function getTracker(trackerNamespace: string): ReactNativeTracker | undefined { + return initializedTrackers[trackerNamespace]?.tracker; +} + +/** + * Removes a tracker given its namespace + * + * @param trackerNamespace {string} + */ +export function removeTracker(trackerNamespace: string): void { + if (initializedTrackers[trackerNamespace]) { + initializedTrackers[trackerNamespace]?.core.deactivate(); + delete initializedTrackers[trackerNamespace]; + } +} + +/** + * Removes all initialized trackers + * + * @returns - A boolean promise + */ +export function removeAllTrackers(): void { + Object.keys(initializedTrackers).forEach(removeTracker); } diff --git a/trackers/react-native-tracker/test/tracker.test.ts b/trackers/react-native-tracker/test/tracker.test.ts index 147d045f8..2305fb1a1 100644 --- a/trackers/react-native-tracker/test/tracker.test.ts +++ b/trackers/react-native-tracker/test/tracker.test.ts @@ -1,4 +1,4 @@ -import { newTracker } from '../src'; +import { getTracker, newTracker, removeAllTrackers, removeTracker } from '../src'; function createMockFetch(status: number, requests: Request[]) { return async (input: Request) => { @@ -21,6 +21,26 @@ describe('Tracker', () => { expect(await newTracker({ namespace: 'test', endpoint: 'http://localhost:9090' })).toBeDefined(); }); + it('retrieves an existing tracker', async () => { + const tracker = await newTracker({ namespace: 'test', endpoint: 'http://localhost:9090' }); + expect(getTracker('test')).toBe(tracker); + expect(getTracker('non-existent')).toBeUndefined(); + }); + + it('removes a tracker', async () => { + await newTracker({ namespace: 'test', endpoint: 'http://localhost:9090' }); + expect(getTracker('test')).toBeDefined(); + removeTracker('test'); + expect(getTracker('test')).toBeUndefined(); + }); + + it('removes all trackers', async () => { + await newTracker({ namespace: 'test', endpoint: 'http://localhost:9090' }); + expect(getTracker('test')).toBeDefined(); + removeAllTrackers(); + expect(getTracker('test')).toBeUndefined(); + }); + it('tracks a page view event with tracker properties', async () => { const tracker = await newTracker({ namespace: 'test',