Skip to content

Commit 5e3f077

Browse files
authored
bug/analytics reset no longer clears anonoymousId (#655)
1 parent 2dfc53d commit 5e3f077

File tree

8 files changed

+210
-37
lines changed

8 files changed

+210
-37
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import unfetch from 'unfetch'
2+
import { AnalyticsBrowser } from '..'
3+
import {
4+
clearAjsBrowserStorage,
5+
getAnonId,
6+
} from '../../test-helpers/browser-storage'
7+
import { createSuccess } from '../../test-helpers/factories'
8+
9+
jest.mock('unfetch')
10+
const helpers = {
11+
mockFetchSettingsSuccessResponse: () => {
12+
return jest
13+
.mocked(unfetch)
14+
.mockImplementation(() => createSuccess({ integrations: {} }))
15+
},
16+
loadAnalytics() {
17+
return AnalyticsBrowser.load({ writeKey: 'foo' })
18+
},
19+
}
20+
21+
beforeEach(() => {
22+
helpers.mockFetchSettingsSuccessResponse()
23+
})
24+
25+
describe('anonymousId', () => {
26+
describe('setting implicitly', () => {
27+
it('is set if an event like track is called during pre-init period', async () => {
28+
const analytics = helpers.loadAnalytics()
29+
const ctx = await analytics.track('foo')
30+
const id = getAnonId()
31+
expect(id).toBeDefined()
32+
expect(ctx.event.anonymousId).toBe(id)
33+
})
34+
35+
it('sets the global anonymousId to the anonymousId set by the most recent event', async () => {
36+
const analytics = helpers.loadAnalytics()
37+
const trackCtx = await analytics.track(
38+
'add to cart',
39+
{},
40+
{ anonymousId: 'foo' }
41+
)
42+
expect(getAnonId()).toBe('foo')
43+
expect(trackCtx.event.anonymousId).toBe('foo')
44+
const idCtx = await analytics.identify('john')
45+
expect(getAnonId()).toBe('foo')
46+
expect(idCtx.event.anonymousId).toBe('foo')
47+
})
48+
})
49+
50+
describe('reset', () => {
51+
beforeEach(() => {
52+
clearAjsBrowserStorage()
53+
})
54+
55+
it('clears anonId if reset is called during pre-init period', async () => {
56+
const analytics = helpers.loadAnalytics()
57+
const track = analytics.track('foo')
58+
const reset = analytics.reset()
59+
await Promise.all([track, reset])
60+
expect(getAnonId()).toBeFalsy()
61+
})
62+
63+
it('clears anonId if reset is called after initialization is complete', async () => {
64+
const [analytics] = await helpers.loadAnalytics()
65+
const track = analytics.track('foo')
66+
expect(typeof getAnonId()).toBe('string')
67+
analytics.reset()
68+
expect(getAnonId()).toBeFalsy()
69+
await track
70+
expect(getAnonId()).toBeFalsy()
71+
})
72+
})
73+
})

packages/browser/src/browser/__tests__/integration.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '../../test-helpers/test-writekeys'
1818
import { PriorityQueue } from '../../lib/priority-queue'
1919
import { getCDN, setGlobalCDNUrl } from '../../lib/parse-cdn'
20+
import { clearAjsBrowserStorage } from '../../test-helpers/browser-storage'
2021

2122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2223
let fetchCalls: Array<any>[] = []
@@ -595,6 +596,10 @@ describe('pageview', () => {
595596
})
596597

597598
describe('setAnonymousId', () => {
599+
beforeEach(() => {
600+
clearAjsBrowserStorage()
601+
})
602+
598603
it('calling setAnonymousId will set a new anonymousId and returns it', async () => {
599604
const [analytics] = await AnalyticsBrowser.load({
600605
writeKey,

packages/browser/src/core/analytics/__tests__/integration.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { PriorityQueue } from '../../../lib/priority-queue'
22
import { MiddlewareParams } from '../../../plugins/middleware'
3-
import { retrieveStoredData } from '../../../test-helpers/retrieve-stored-data'
3+
import {
4+
getAjsBrowserStorage,
5+
clearAjsBrowserStorage,
6+
} from '../../../test-helpers/browser-storage'
47
import { Context } from '../../context'
58
import { Plugin } from '../../plugin'
69
import { EventQueue } from '../../queue/event-queue'
@@ -224,20 +227,22 @@ describe('Analytics', () => {
224227
})
225228

226229
describe('reset', () => {
230+
beforeEach(() => {
231+
clearAjsBrowserStorage()
232+
})
233+
227234
it('clears user and group data', async () => {
228235
const analytics = new Analytics({ writeKey: '' })
229236

230-
const cookieNames = ['ajs_user_id', 'ajs_anonymous_id', 'ajs_group_id']
231-
const localStorageKeys = ['ajs_user_traits', 'ajs_group_properties']
232-
233237
analytics.user().anonymousId('unknown-user')
234238
analytics.user().id('known-user')
235239
analytics.user().traits({ job: 'engineer' })
236240
analytics.group().id('known-group')
237241
analytics.group().traits({ team: 'analytics' })
238242

239243
// Ensure all cookies/localstorage is written correctly first
240-
let storedData = retrieveStoredData({ cookieNames, localStorageKeys })
244+
245+
let storedData = getAjsBrowserStorage()
241246
expect(storedData).toEqual({
242247
ajs_user_id: 'known-user',
243248
ajs_anonymous_id: 'unknown-user',
@@ -252,7 +257,7 @@ describe('Analytics', () => {
252257

253258
// Now make sure everything was cleared on reset
254259
analytics.reset()
255-
storedData = retrieveStoredData({ cookieNames, localStorageKeys })
260+
storedData = getAjsBrowserStorage()
256261
expect(storedData).toEqual({})
257262
})
258263
})

packages/browser/src/core/events/__tests__/index.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,48 @@ describe('Event Factory', () => {
322322
innerProp: '👻',
323323
})
324324
})
325+
326+
describe.skip('anonymousId', () => {
327+
// TODO: the code should be fixed so that these tests can pass -- this eventFactory does not seem to handle these edge cases well.
328+
// When an event is dispatched, there are four places anonymousId can live: event.anonymousId, event.options.anonymousId, event.context.anonymousId, and the user object / localStorage.
329+
// It would be good to have a source of truth
330+
test('accepts an anonymousId', () => {
331+
const track = factory.track('Order Completed', shoes, {
332+
anonymousId: 'foo',
333+
})
334+
expect(track.anonymousId).toBe('foo')
335+
expect(track.context?.anonymousId).toBe('foo')
336+
})
337+
338+
test('custom passed anonymousId should set global user instance', () => {
339+
const id = Math.random().toString()
340+
factory.track('Order Completed', shoes, {
341+
anonymousId: id,
342+
})
343+
expect(user.anonymousId()).toBe(id)
344+
})
345+
346+
test('if two different anonymousIds are passed, should use one on the event', () => {
347+
const track = factory.track('Order Completed', shoes, {
348+
anonymousId: 'bar',
349+
context: {
350+
anonymousId: 'foo',
351+
},
352+
})
353+
expect(track.context?.anonymousId).toBe('bar')
354+
expect(track.anonymousId).toBe('bar')
355+
})
356+
357+
test('should set an anonymousId passed from the context on the event', () => {
358+
const track = factory.track('Order Completed', shoes, {
359+
context: {
360+
anonymousId: 'foo',
361+
},
362+
})
363+
expect(track.context?.anonymousId).toBe('foo')
364+
expect(track.anonymousId).toBe('foo')
365+
})
366+
})
325367
})
326368

327369
describe('normalize', function () {

packages/browser/src/core/events/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ export class EventFactory {
205205
}
206206

207207
public normalize(event: SegmentEvent): SegmentEvent {
208+
// set anonymousId globally if we encounter an override
209+
//segment.com/docs/connections/sources/catalog/libraries/website/javascript/identity/#override-the-anonymous-id-using-the-options-object
210+
event.options?.anonymousId &&
211+
this.user.anonymousId(event.options.anonymousId)
212+
208213
const integrationBooleans = Object.keys(event.integrations ?? {}).reduce(
209214
(integrationNames, name) => {
210215
return {

packages/browser/src/plugins/segmentio/normalize.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ export function normalize(
124124

125125
json.context = json.context ?? json.options ?? {}
126126
const ctx = json.context
127-
const anonId = json.anonymousId
128127

129128
delete json.options
130129
json.writeKey = settings?.apiKey
@@ -159,7 +158,8 @@ export function normalize(
159158
referrerId(query, ctx, analytics.options.disableClientPersistence ?? false)
160159

161160
json.userId = json.userId || user.id()
162-
json.anonymousId = user.anonymousId(anonId)
161+
json.anonymousId = json.anonymousId || user.anonymousId()
162+
163163
json.sentAt = new Date()
164164

165165
const failed = analytics.queue.failedInitializations || []
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import cookie from 'js-cookie'
2+
3+
const ajsCookieNames = [
4+
'ajs_user_id',
5+
'ajs_anonymous_id',
6+
'ajs_group_id',
7+
] as const
8+
const ajsLocalStorageKeys = ['ajs_user_traits', 'ajs_group_properties'] as const
9+
10+
export const getAjsBrowserStorage = () => {
11+
return getBrowserStorage({
12+
cookieNames: ajsCookieNames,
13+
localStorageKeys: ajsLocalStorageKeys,
14+
})
15+
}
16+
17+
export const getAnonId = () => getAjsBrowserStorage().ajs_anonymous_id
18+
19+
export const clearAjsBrowserStorage = () => {
20+
return clearBrowserStorage({
21+
cookieNames: ajsCookieNames,
22+
localStorageKeys: ajsLocalStorageKeys,
23+
})
24+
}
25+
26+
export function getBrowserStorage<
27+
CookieNames extends string,
28+
LSKeys extends string
29+
>({
30+
cookieNames,
31+
localStorageKeys,
32+
}: {
33+
cookieNames: readonly CookieNames[]
34+
localStorageKeys: readonly LSKeys[]
35+
}): Record<CookieNames | LSKeys, string | {}> {
36+
const result = {} as ReturnType<typeof getBrowserStorage>
37+
38+
const cookies = cookie.get()
39+
cookieNames.forEach((name) => {
40+
if (name in cookies) {
41+
result[name] = cookies[name]
42+
}
43+
})
44+
45+
localStorageKeys.forEach((key) => {
46+
const value = localStorage.getItem(key)
47+
if (value !== null && typeof value !== 'undefined') {
48+
result[key] = JSON.parse(value)
49+
}
50+
})
51+
52+
return result
53+
}
54+
55+
export function clearBrowserStorage({
56+
cookieNames,
57+
localStorageKeys, // if no keys are passed, the entire thing is cleared
58+
}: {
59+
cookieNames: string[] | readonly string[]
60+
localStorageKeys?: string[] | readonly string[]
61+
}) {
62+
cookieNames.forEach((name) => {
63+
cookie.remove(name)
64+
})
65+
if (!localStorageKeys) {
66+
localStorage.clear()
67+
} else {
68+
localStorageKeys.forEach((key) => {
69+
localStorage.removeItem(key)
70+
})
71+
}
72+
}

packages/browser/src/test-helpers/retrieve-stored-data.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)