diff --git a/.changeset/flat-carrots-accept.md b/.changeset/flat-carrots-accept.md new file mode 100644 index 000000000..fb4e11cd4 --- /dev/null +++ b/.changeset/flat-carrots-accept.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': minor +--- + +Device mode destination filters will now filter properties within arrays, just like they do in cloud mode diff --git a/.changeset/violet-rockets-tie.md b/.changeset/violet-rockets-tie.md new file mode 100644 index 000000000..f9e8f541a --- /dev/null +++ b/.changeset/violet-rockets-tie.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': patch +--- + +Fixes issue related to how retried events are stored in localStorage to prevent analytics.js from reading events for a different writeKey when that writeKey is used on the same domain as the current analytics.js. diff --git a/packages/browser-integration-tests/src/fixtures/settings.ts b/packages/browser-integration-tests/src/fixtures/settings.ts index e6e27a86e..b31478442 100644 --- a/packages/browser-integration-tests/src/fixtures/settings.ts +++ b/packages/browser-integration-tests/src/fixtures/settings.ts @@ -4,11 +4,11 @@ type RemotePlugin = NonNullable[number] export class SettingsBuilder { private settings: Record - constructor(baseSettings?: Record) { + constructor(writeKey: string, baseSettings?: Record) { this.settings = baseSettings || { integrations: { 'Segment.io': { - apiKey: 'writeKey', + apiKey: writeKey, unbundledIntegrations: [], addBundledMetadata: true, maybeBundledConfigIds: {}, diff --git a/packages/browser-integration-tests/src/helpers/extract-writekey.ts b/packages/browser-integration-tests/src/helpers/extract-writekey.ts new file mode 100644 index 000000000..888e61c67 --- /dev/null +++ b/packages/browser-integration-tests/src/helpers/extract-writekey.ts @@ -0,0 +1,9 @@ +export function extractWriteKeyFromUrl(url: string): string | undefined { + const matches = url.match( + /https:\/\/cdn.segment.com\/v1\/projects\/(.+)\/settings/ + ) + + if (matches) { + return matches[1] + } +} diff --git a/packages/browser-integration-tests/src/helpers/get-persisted-items.ts b/packages/browser-integration-tests/src/helpers/get-persisted-items.ts new file mode 100644 index 000000000..7249dfd81 --- /dev/null +++ b/packages/browser-integration-tests/src/helpers/get-persisted-items.ts @@ -0,0 +1,42 @@ +export interface PersistedQueueResult { + key: string + name: string + messageIds: string[] + writeKey?: string +} + +export function getPersistedItems(): PersistedQueueResult[] { + const results: PersistedQueueResult[] = [] + + for (let i = 0; i < window.localStorage.length; i++) { + const key = window.localStorage.key(i) + if ( + key && + key.startsWith('persisted-queue:v1:') && + key.endsWith(':items') + ) { + const value = window.localStorage.getItem(key) + const messageIds = value + ? JSON.parse(value).map((i: any) => i.event.messageId) + : [] + + // Key looks like either: + // new keys - persisted-queue:v1:writeKey:dest-Segment.io:items + // old keys - persisted-queue:v1:dest-Segment.io:items + const components = key.split(':') + let writeKey: string | undefined + let name: string + + if (components.length === 5) { + ;[, , writeKey, name] = components + } else if (components.length === 4) { + ;[, , name] = components + } else { + throw new Error('Unrecognized persisted queue key.') + } + results.push({ key, messageIds, name, writeKey }) + } + } + + return results +} diff --git a/packages/browser-integration-tests/src/index.test.ts b/packages/browser-integration-tests/src/index.test.ts index d5fe69a2d..aaa9d1758 100644 --- a/packages/browser-integration-tests/src/index.test.ts +++ b/packages/browser-integration-tests/src/index.test.ts @@ -1,6 +1,7 @@ import { test, expect } from '@playwright/test' import { SettingsBuilder } from './fixtures/settings' import { standaloneMock } from './helpers/standalone-mock' +import { extractWriteKeyFromUrl } from './helpers/extract-writekey' test.describe('Standalone tests', () => { test.beforeEach(standaloneMock) @@ -14,13 +15,14 @@ test.describe('Standalone tests', () => { return route.continue() } + const writeKey = extractWriteKeyFromUrl(request.url()) || 'writeKey' return route.fulfill({ status: 200, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify( - new SettingsBuilder() + new SettingsBuilder(writeKey) .addActionDestinationSettings({ name: 'Amplitude (Actions)', creationName: 'Actions Amplitude', @@ -74,13 +76,14 @@ test.describe('Standalone tests', () => { return route.continue() } + const writeKey = extractWriteKeyFromUrl(request.url()) || 'writeKey' return route.fulfill({ status: 200, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify( - new SettingsBuilder() + new SettingsBuilder(writeKey) .addActionDestinationSettings({ name: 'Braze Cloud Mode (Actions)', creationName: 'Braze Cloud Mode (Actions)', diff --git a/packages/browser-integration-tests/src/segment-retries.test.ts b/packages/browser-integration-tests/src/segment-retries.test.ts index b066d046f..f10442b2a 100644 --- a/packages/browser-integration-tests/src/segment-retries.test.ts +++ b/packages/browser-integration-tests/src/segment-retries.test.ts @@ -1,115 +1,201 @@ import { Request, test, expect } from '@playwright/test' import { SettingsBuilder } from './fixtures/settings' import { standaloneMock } from './helpers/standalone-mock' +import { extractWriteKeyFromUrl } from './helpers/extract-writekey' +import { + PersistedQueueResult, + getPersistedItems, +} from './helpers/get-persisted-items' -test.describe('Standalone tests', () => { +test.describe('Segment.io Retries', () => { test.beforeEach(standaloneMock) - - test.describe('Segment.io Retries', () => { - test.beforeEach(async ({ context }) => { - await context.route( - 'https://cdn.segment.com/v1/projects/*/settings', - (route, request) => { - if (request.method().toLowerCase() !== 'get') { - return route.continue() - } - - return route.fulfill({ - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(new SettingsBuilder().build()), - }) + test.beforeEach(async ({ context }) => { + await context.route( + 'https://cdn.segment.com/v1/projects/*/settings', + (route, request) => { + if (request.method().toLowerCase() !== 'get') { + return route.continue() } - ) + + const writeKey = extractWriteKeyFromUrl(request.url()) || 'writeKey' + return route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(new SettingsBuilder(writeKey).build()), + }) + } + ) + }) + test.beforeEach(async ({ context }) => { + await context.setOffline(false) + }) + + test('supports retrying failed requests on page navigation', async ({ + page, + }) => { + // Load analytics.js + await page.goto('/standalone.html') + await page.evaluate(() => window.analytics.load('fake-key')) + + // fail the 1st request + await page.route( + 'https://api.segment.io/v1/t', + (route) => { + return route.abort('blockedbyclient') + }, + { + times: 1, + } + ) + const requestFailure = new Promise>((resolve) => { + page.once('requestfailed', (request) => resolve(request.postDataJSON())) }) - test('supports retrying failed requests on page navigation', async ({ - page, - }) => { - // Load analytics.js - await page.goto('/standalone.html') - await page.evaluate(() => window.analytics.load('fake-key')) - - // fail the 1st request - await page.route( - 'https://api.segment.io/v1/t', - (route) => { - return route.abort('blockedbyclient') - }, - { - times: 1, - } - ) - const requestFailure = new Promise>((resolve) => { - page.once('requestfailed', (request) => resolve(request.postDataJSON())) - }) - - // trigger an event - await page.evaluate(() => { - void window.analytics.track('test event') - }) - - const { messageId } = await requestFailure - await page.reload() - - // load analytics.js again and wait for a new request. - const [request] = await Promise.all([ - page.waitForRequest('https://api.segment.io/v1/t'), - page.evaluate(() => window.analytics.load('fake-key')), - ]) - - expect(request.method()).toBe('POST') - expect(request.postDataJSON().messageId).toBe(messageId) + // trigger an event + await page.evaluate(() => { + void window.analytics.track('test event') }) - test('supports retrying in-flight requests on page navigation', async ({ - page, - }) => { - // Load analytics.js - await page.goto('/standalone.html') - await page.evaluate(() => window.analytics.load('fake-key')) - - // blackhole the request so that it stays in-flight when we reload the page - await page.route( - 'https://api.segment.io/v1/t', - async () => { - // do nothing - }, - { - times: 1, - } - ) - - // Detect when we've seen a track request initiated by the browser - const requestSent = new Promise>((resolve) => { - const onRequest: (req: Request) => void = (req) => { - if (req.url() === 'https://api.segment.io/v1/t') { - page.off('request', onRequest) - resolve(req.postDataJSON()) - } + const { messageId } = await requestFailure + await page.reload() + + // load analytics.js again and wait for a new request. + const [request] = await Promise.all([ + page.waitForRequest('https://api.segment.io/v1/t'), + page.evaluate(() => window.analytics.load('fake-key')), + ]) + + expect(request.method()).toBe('POST') + expect(request.postDataJSON().messageId).toBe(messageId) + }) + + test('supports retrying in-flight requests on page navigation', async ({ + page, + }) => { + // Load analytics.js + await page.goto('/standalone.html') + await page.evaluate(() => window.analytics.load('fake-key')) + + // blackhole the request so that it stays in-flight when we reload the page + await page.route( + 'https://api.segment.io/v1/t', + async () => { + // do nothing + }, + { + times: 1, + } + ) + + // Detect when we've seen a track request initiated by the browser + const requestSent = new Promise>((resolve) => { + const onRequest: (req: Request) => void = (req) => { + if (req.url() === 'https://api.segment.io/v1/t') { + page.off('request', onRequest) + resolve(req.postDataJSON()) } + } + + page.on('request', onRequest) + }) + + // trigger an event + await page.evaluate(() => { + void window.analytics.track('test event') + }) + + const { messageId } = await requestSent + await page.reload() - page.on('request', onRequest) - }) + // load analytics.js again and wait for a new request. + const [request] = await Promise.all([ + page.waitForRequest('https://api.segment.io/v1/t'), + page.evaluate(() => window.analytics.load('fake-key')), + ]) - // trigger an event - await page.evaluate(() => { - void window.analytics.track('test event') - }) + expect(request.method()).toBe('POST') + expect(request.postDataJSON().messageId).toBe(messageId) + }) + + test('events persisted to localStorage do not leak across write keys', async ({ + page, + }) => { + // Load analytics.js + await page.goto('/standalone.html') + await page.evaluate(() => window.analytics.load('key1')) + + // fail the initial track request on first 2 page loads (2 different write keys) + await page.route( + 'https://api.segment.io/v1/t', + (route) => { + return route.abort('blockedbyclient') + }, + { + times: 2, + } + ) + let requestFailure = new Promise>((resolve) => { + page.once('requestfailed', (request) => resolve(request.postDataJSON())) + }) + + // trigger an event that wil fail + await page.evaluate(() => { + void window.analytics.track('test event') + }) + + const { messageId: messageId1 } = await requestFailure + await page.reload() + + // check localstorage for event data from previous analytics (key1) + let persistedItems = await page.evaluate(getPersistedItems) + + expect(persistedItems).toHaveLength(1) + expect(persistedItems[0].writeKey).toBe('key1') + expect(persistedItems[0].messageIds).toStrictEqual([messageId1]) - const { messageId } = await requestSent - await page.reload() + requestFailure = new Promise>((resolve) => { + page.once('requestfailed', (request) => resolve(request.postDataJSON())) + }) - // load analytics.js again and wait for a new request. - const [request] = await Promise.all([ - page.waitForRequest('https://api.segment.io/v1/t'), - page.evaluate(() => window.analytics.load('fake-key')), - ]) + // Load analytics with a different write key (key2) + await page.evaluate(() => window.analytics.load('key2')) - expect(request.method()).toBe('POST') - expect(request.postDataJSON().messageId).toBe(messageId) + // trigger an event that will fail + await page.evaluate(() => { + void window.analytics.track('test event') }) + + const { messageId: messageId2 } = await requestFailure + await page.reload() + + // check localstorage for data from both write keys + persistedItems = await page.evaluate(getPersistedItems) + + expect(persistedItems).toHaveLength(2) + const persistedByWriteKey = persistedItems.reduce((prev, cur) => { + prev[cur.writeKey || '_'] = cur + return prev + }, {} as { [writeKey: string]: PersistedQueueResult }) + expect(persistedByWriteKey['key1'].messageIds).toStrictEqual([messageId1]) + expect(persistedByWriteKey['key2'].messageIds).toStrictEqual([messageId2]) + + // Now load analytics with original write key (key1) to validate message is sent + const [request] = await Promise.all([ + page.waitForRequest('https://api.segment.io/v1/t'), + page.evaluate(() => window.analytics.load('key1')), + ]) + + expect(request.method()).toBe('POST') + expect(request.postDataJSON().messageId).toBe(messageId1) + expect(request.postDataJSON().writeKey).toBe('key1') + + // Make sure localStorage only has data for the 2nd writeKey - which wasn't reloaded. + persistedItems = await page.evaluate(getPersistedItems) + + expect(persistedItems).toHaveLength(1) + expect(persistedItems[0].writeKey).toBe('key2') + expect(persistedItems[0].messageIds).toStrictEqual([messageId2]) }) }) diff --git a/packages/browser/package.json b/packages/browser/package.json index b65869e0d..446bdf59d 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -51,7 +51,7 @@ "@segment/analytics-core": "1.2.5", "@segment/analytics.js-video-plugins": "^0.2.1", "@segment/facade": "^3.4.9", - "@segment/tsub": "1.0.1", + "@segment/tsub": "^2.0.0", "dset": "^3.1.2", "js-cookie": "3.0.1", "node-fetch": "^2.6.7", diff --git a/packages/browser/src/browser/__tests__/integration.test.ts b/packages/browser/src/browser/__tests__/integration.test.ts index 73d013d06..daff261ab 100644 --- a/packages/browser/src/browser/__tests__/integration.test.ts +++ b/packages/browser/src/browser/__tests__/integration.test.ts @@ -718,6 +718,7 @@ describe('addDestinationMiddleware', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, @@ -861,6 +862,7 @@ describe('deregister', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, @@ -1056,6 +1058,7 @@ describe('Options', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, @@ -1092,6 +1095,7 @@ describe('Options', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, @@ -1128,6 +1132,7 @@ describe('Options', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, diff --git a/packages/browser/src/browser/__tests__/integrations.integration.test.ts b/packages/browser/src/browser/__tests__/integrations.integration.test.ts index 3f80931b2..a70f12095 100644 --- a/packages/browser/src/browser/__tests__/integrations.integration.test.ts +++ b/packages/browser/src/browser/__tests__/integrations.integration.test.ts @@ -59,6 +59,7 @@ describe('Integrations', () => { const amplitude = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, @@ -129,13 +130,20 @@ describe('Integrations', () => { const amplitude = new LegacyDestination( 'Amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, {} ) - const ga = new LegacyDestination('Google-Analytics', 'latest', {}, {}) + const ga = new LegacyDestination( + 'Google-Analytics', + 'latest', + writeKey, + {}, + {} + ) const [analytics] = await AnalyticsBrowser.load({ writeKey, @@ -156,14 +164,27 @@ describe('Integrations', () => { const amplitude = new LegacyDestination( 'Amplitude', 'latest', + writeKey, { apiKey: amplitudeWriteKey, }, {} ) - const ga = new LegacyDestination('Google-Analytics', 'latest', {}, {}) - const customerIO = new LegacyDestination('Customer.io', 'latest', {}, {}) + const ga = new LegacyDestination( + 'Google-Analytics', + 'latest', + writeKey, + {}, + {} + ) + const customerIO = new LegacyDestination( + 'Customer.io', + 'latest', + writeKey, + {}, + {} + ) const [analytics] = await AnalyticsBrowser.load({ writeKey, diff --git a/packages/browser/src/browser/__tests__/standalone-analytics.test.ts b/packages/browser/src/browser/__tests__/standalone-analytics.test.ts index eec550fce..ede23fc4b 100644 --- a/packages/browser/src/browser/__tests__/standalone-analytics.test.ts +++ b/packages/browser/src/browser/__tests__/standalone-analytics.test.ts @@ -27,7 +27,9 @@ jest.mock('../../core/analytics', () => ({ register, emit: jest.fn(), on, - queue: new EventQueue(new PersistedPriorityQueue(1, 'event-queue') as any), + queue: new EventQueue( + new PersistedPriorityQueue(1, 'foo:event-queue') as any + ), options, }), })) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 54695779e..b85fba41e 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -150,6 +150,7 @@ async function flushFinalBuffer( } async function registerPlugins( + writeKey: string, legacySettings: LegacySettings, analytics: Analytics, opts: InitOptions, @@ -173,6 +174,7 @@ async function registerPlugins( /* webpackChunkName: "ajs-destination" */ '../plugins/ajs-destination' ).then((mod) => { return mod.ajsDestinations( + writeKey, legacySettings, analytics.integrations, opts, @@ -286,6 +288,7 @@ async function loadAnalytics( flushPreBuffer(analytics, preInitBuffer) const ctx = await registerPlugins( + settings.writeKey, legacySettings, analytics, opts, diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index f4535e853..507264e58 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -57,11 +57,15 @@ const deprecationWarning = const global: any = getGlobal() const _analytics = global?.analytics -function createDefaultQueue(retryQueue = false, disablePersistance = false) { +function createDefaultQueue( + name: string, + retryQueue = false, + disablePersistance = false +) { const maxAttempts = retryQueue ? 4 : 1 const priorityQueue = disablePersistance ? new PriorityQueue(maxAttempts, []) - : new PersistedPriorityQueue(maxAttempts, 'event-queue') + : new PersistedPriorityQueue(maxAttempts, name) return new EventQueue(priorityQueue) } @@ -145,7 +149,12 @@ export class Analytics this.settings = settings this.settings.timeout = this.settings.timeout ?? 300 this.queue = - queue ?? createDefaultQueue(options?.retryQueue, disablePersistance) + queue ?? + createDefaultQueue( + `${settings.writeKey}:event-queue`, + options?.retryQueue, + disablePersistance + ) this._universalStorage = new UniversalStorage( disablePersistance ? ['memory'] : ['localStorage', 'cookie', 'memory'], diff --git a/packages/browser/src/core/queue/__tests__/event-queue.test.ts b/packages/browser/src/core/queue/__tests__/event-queue.test.ts index 06d5e0ef5..860c5bc36 100644 --- a/packages/browser/src/core/queue/__tests__/event-queue.test.ts +++ b/packages/browser/src/core/queue/__tests__/event-queue.test.ts @@ -47,7 +47,7 @@ const segmentio = { describe('alternative names', () => { test('delivers to action destinations using alternative names', async () => { - const eq = new EventQueue() + const eq = new EventQueue(`writeKey:event-queue`) const fullstory = new ActionDestination('fullstory', testPlugin) // TODO: This should be re-written as higher level integration test. fullstory.alternativeNames.push('fullstory trackEvent') fullstory.type = 'destination' diff --git a/packages/browser/src/core/queue/event-queue.ts b/packages/browser/src/core/queue/event-queue.ts index d4c9989fb..299508fac 100644 --- a/packages/browser/src/core/queue/event-queue.ts +++ b/packages/browser/src/core/queue/event-queue.ts @@ -6,8 +6,14 @@ import { CoreEventQueue } from '@segment/analytics-core' import { isOffline } from '../connection' export class EventQueue extends CoreEventQueue { - constructor(priorityQueue?: PriorityQueue) { - super(priorityQueue ?? new PersistedPriorityQueue(4, 'event-queue')) + constructor(name: string) + constructor(priorityQueue: PriorityQueue) + constructor(nameOrQueue: string | PriorityQueue) { + super( + typeof nameOrQueue === 'string' + ? new PersistedPriorityQueue(4, nameOrQueue) + : nameOrQueue + ) } async flush(): Promise { if (isOffline()) return [] diff --git a/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts b/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts index d4dfcf9a1..5a12d8249 100644 --- a/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts +++ b/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts @@ -74,6 +74,7 @@ jest.mock('unfetch', () => { }) describe('loading ajsDestinations', () => { + const writeKey = 'foo' beforeEach(async () => { jest.resetAllMocks() @@ -84,7 +85,7 @@ describe('loading ajsDestinations', () => { }) it('loads version overrides', () => { - const destinations = ajsDestinations(cdnResponse, {}, {}) + const destinations = ajsDestinations(writeKey, cdnResponse, {}, {}) const withVersionSettings = destinations.find( (d) => d.name === 'WithVersionSettings' @@ -103,7 +104,7 @@ describe('loading ajsDestinations', () => { // This test should temporary. It must be deleted once we fix the Iterable metadata it('ignores Iterable', () => { - const destinations = ajsDestinations(cdnResponse, {}, {}) + const destinations = ajsDestinations(writeKey, cdnResponse, {}, {}) const iterable = destinations.find((d) => d.name === 'Iterable') expect(iterable).toBeUndefined() }) @@ -111,6 +112,7 @@ describe('loading ajsDestinations', () => { describe('versionSettings.components', () => { it('ignores [componentType:browser] when bundlingStatus is unbundled', () => { const destinations = ajsDestinations( + writeKey, { integrations: { 'Some server destination': { @@ -141,6 +143,7 @@ describe('loading ajsDestinations', () => { it('loads [componentType:browser] when bundlingStatus is not defined', () => { const destinations = ajsDestinations( + writeKey, { integrations: { 'Some server destination': { @@ -170,13 +173,14 @@ describe('loading ajsDestinations', () => { }) it('loads type:browser legacy ajs destinations from cdn', () => { - const destinations = ajsDestinations(cdnResponse, {}, {}) + const destinations = ajsDestinations(writeKey, cdnResponse, {}, {}) // ignores segment.io expect(destinations.length).toBe(5) }) it('ignores type:browser when bundlingStatus is unbundled', () => { const destinations = ajsDestinations( + writeKey, { integrations: { 'Some server destination': { @@ -201,6 +205,7 @@ describe('loading ajsDestinations', () => { it('loads type:browser when bundlingStatus is not defined', () => { const destinations = ajsDestinations( + writeKey, { integrations: { 'Some server destination': { @@ -223,12 +228,13 @@ describe('loading ajsDestinations', () => { }) it('ignores destinations of type:server', () => { - const destinations = ajsDestinations(cdnResponse, {}, {}) + const destinations = ajsDestinations(writeKey, cdnResponse, {}, {}) expect(destinations.find((d) => d.name === 'Zapier')).toBe(undefined) }) it('does not load integrations when All:false', () => { const destinations = ajsDestinations( + writeKey, cdnResponse, { All: false, @@ -240,6 +246,7 @@ describe('loading ajsDestinations', () => { it('loads integrations when All:false, : true', () => { const destinations = ajsDestinations( + writeKey, cdnResponse, { All: false, @@ -256,7 +263,13 @@ describe('loading ajsDestinations', () => { const middleware = tsubMiddleware( cdnResponse.middlewareSettings!.routingRules ) - const destinations = ajsDestinations(cdnResponse, {}, {}, middleware) + const destinations = ajsDestinations( + writeKey, + cdnResponse, + {}, + {}, + middleware + ) const amplitude = destinations.find((d) => d.name === 'Amplitude') expect(amplitude?.middleware.length).toBe(1) }) @@ -267,6 +280,7 @@ describe('settings', () => { const dest = new LegacyDestination( 'Yandex', 'latest', + 'writeKey', { type: 'custom', }, @@ -279,6 +293,7 @@ describe('settings', () => { const dest = new LegacyDestination( 'Amplitude', 'latest', + 'writeKey', { type: 'browser', }, @@ -294,18 +309,21 @@ describe('options', () => { const defaultDestWithPersistance = new LegacyDestination( 'LocalStorageUser', 'latest', + 'writeKey', {}, {} ) const destWithPersistance = new LegacyDestination( 'LocalStorageUserToo', 'latest', + 'writeKey', {}, { disableClientPersistence: false } ) const destWithoutPersistance = new LegacyDestination( 'MemoryUser', 'latest', + 'writeKey', {}, { disableClientPersistence: true } ) @@ -326,13 +344,15 @@ describe('remote loading', () => { const loadAmplitude = async ( obfuscate = false ): Promise => { + const writeKey = 'abc' const ajs = new Analytics({ - writeKey: 'abc', + writeKey, }) const dest = new LegacyDestination( 'Amplitude', 'latest', + writeKey, { apiKey: AMPLITUDE_WRITEKEY, }, @@ -499,13 +519,15 @@ describe('plan', () => { }) const loadAmplitude = async (plan: Plan): Promise => { + const writeKey = 'abc' const ajs = new Analytics({ - writeKey: 'abc', + writeKey, }) const dest = new LegacyDestination( 'amplitude', 'latest', + writeKey, { apiKey: AMPLITUDE_WRITEKEY, }, @@ -777,7 +799,12 @@ describe('option overrides', () => { }, } - const destinations = ajsDestinations(cdnSettings, {}, initOptions) + const destinations = ajsDestinations( + 'writeKey', + cdnSettings, + {}, + initOptions + ) const amplitude = destinations[0] await amplitude.load(Context.system(), {} as Analytics) diff --git a/packages/browser/src/plugins/ajs-destination/index.ts b/packages/browser/src/plugins/ajs-destination/index.ts index 9c7450079..ac2a938cf 100644 --- a/packages/browser/src/plugins/ajs-destination/index.ts +++ b/packages/browser/src/plugins/ajs-destination/index.ts @@ -86,6 +86,7 @@ export class LegacyDestination implements DestinationPlugin { constructor( name: string, version: string, + writeKey: string, settings: JSONObject = {}, options: InitOptions, integrationSource?: ClassicIntegrationSource @@ -105,7 +106,7 @@ export class LegacyDestination implements DestinationPlugin { this.options = options this.buffer = options.disableClientPersistence ? new PriorityQueue(4, []) - : new PersistedPriorityQueue(4, `dest-${name}`) + : new PersistedPriorityQueue(4, `${writeKey}:dest-${name}`) this.scheduleFlush() } @@ -318,6 +319,7 @@ export class LegacyDestination implements DestinationPlugin { } export function ajsDestinations( + writeKey: string, settings: LegacySettings, globalIntegrations: Integrations = {}, options: InitOptions = {}, @@ -372,6 +374,7 @@ export function ajsDestinations( const destination = new LegacyDestination( name, version, + writeKey, integrationOptions[name], options, adhocIntegrationSources?.[name] diff --git a/packages/browser/src/plugins/middleware/__tests__/index.test.ts b/packages/browser/src/plugins/middleware/__tests__/index.test.ts index 9f01a7232..5c44d8a5a 100644 --- a/packages/browser/src/plugins/middleware/__tests__/index.test.ts +++ b/packages/browser/src/plugins/middleware/__tests__/index.test.ts @@ -118,7 +118,13 @@ describe(sourceMiddlewarePlugin, () => { next(payload) } - const dest = new LegacyDestination('Google Analytics', 'latest', {}, {}) + const dest = new LegacyDestination( + 'Google Analytics', + 'latest', + 'writeKey', + {}, + {} + ) const ctx = new Context({ type: 'track', diff --git a/packages/browser/src/plugins/segmentio/__tests__/retries.test.ts b/packages/browser/src/plugins/segmentio/__tests__/retries.test.ts index 1c19f3231..e73ce2ec9 100644 --- a/packages/browser/src/plugins/segmentio/__tests__/retries.test.ts +++ b/packages/browser/src/plugins/segmentio/__tests__/retries.test.ts @@ -46,7 +46,10 @@ describe('Segment.io retries', () => { value: jest.fn().mockImplementation(() => queue), }) } else { - queue = new PPQ.PersistedPriorityQueue(3, `test-Segment.io`) + queue = new PPQ.PersistedPriorityQueue( + 3, + `${options.apiKey}:test-Segment.io` + ) queue['__type'] = 'persisted' Object.defineProperty(PPQ, 'PersistedPriorityQueue', { writable: true, diff --git a/packages/browser/src/plugins/segmentio/index.ts b/packages/browser/src/plugins/segmentio/index.ts index f4c7e0b08..70a76ea65 100644 --- a/packages/browser/src/plugins/segmentio/index.ts +++ b/packages/browser/src/plugins/segmentio/index.ts @@ -64,11 +64,13 @@ export async function segmentio( inflightEvents.clear() }) + const writeKey = settings?.apiKey ?? '' + const buffer = analytics.options.disableClientPersistence ? new PriorityQueue(analytics.queue.queue.maxAttempts, []) : new PersistedPriorityQueue( analytics.queue.queue.maxAttempts, - `dest-Segment.io` + `${writeKey}:dest-Segment.io` ) const inflightEvents = new Set() diff --git a/yarn.lock b/yarn.lock index 232dc9ec8..8a94c4939 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1737,7 +1737,7 @@ __metadata: "@segment/analytics.js-integration-amplitude": ^3.3.3 "@segment/analytics.js-video-plugins": ^0.2.1 "@segment/facade": ^3.4.9 - "@segment/tsub": 1.0.1 + "@segment/tsub": ^2.0.0 "@size-limit/preset-big-lib": ^7.0.8 "@types/flat": ^5.0.1 "@types/fs-extra": ^9.0.2 @@ -1914,9 +1914,9 @@ __metadata: languageName: node linkType: hard -"@segment/tsub@npm:1.0.1": - version: 1.0.1 - resolution: "@segment/tsub@npm:1.0.1" +"@segment/tsub@npm:^2.0.0": + version: 2.0.0 + resolution: "@segment/tsub@npm:2.0.0" dependencies: "@stdlib/math-base-special-ldexp": ^0.0.5 dlv: ^1.1.3 @@ -1924,7 +1924,7 @@ __metadata: tiny-hashes: ^1.0.1 bin: tsub: dist/index.js - checksum: a5a1cedcc98dc19e09a3cb806906a457ef956a592079a14c8f88041b95fc32c876bc7df04d8c6d34b905fef128fd6dfec7222cf7fbd3302e1a4a47dcd4b8fa1a + checksum: 28e27be4bb7d70ef9e74723b206ab0e6cc4764180c8f8e66266efc88bee6843042d4a969685603dd3ee9d3a5dfac987e7f3b76c0543a869d446d5e16cd960315 languageName: node linkType: hard