Skip to content

Commit 32582be

Browse files
authored
Fix query string api not respecting source middleware (#1207)
1 parent 9883151 commit 32582be

File tree

4 files changed

+75
-40
lines changed

4 files changed

+75
-40
lines changed

.changeset/few-starfishes-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': patch
3+
---
4+
5+
Fix query string API not respecting source middleware

packages/browser/src/browser/__tests__/query-string.integration.test.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import { JSDOM } from 'jsdom'
22
import { Analytics } from '../../core/analytics'
3-
// @ts-ignore loadCDNSettings mocked dependency is accused as unused
43
import { AnalyticsBrowser } from '..'
54
import { setGlobalCDNUrl } from '../../lib/parse-cdn'
65
import { TEST_WRITEKEY } from '../../test-helpers/test-writekeys'
6+
import { createMockFetchImplementation } from '../../test-helpers/fixtures/create-fetch-method'
7+
import { parseFetchCall } from '../../test-helpers/fetch-parse'
8+
import { cdnSettingsKitchenSink } from '../../test-helpers/fixtures/cdn-settings'
9+
10+
const fetchCalls: ReturnType<typeof parseFetchCall>[] = []
11+
jest.mock('unfetch', () => {
12+
return {
13+
__esModule: true,
14+
default: (url: RequestInfo, body?: RequestInit) => {
15+
const call = parseFetchCall([url, body])
16+
fetchCalls.push(call)
17+
return createMockFetchImplementation(cdnSettingsKitchenSink)(url, body)
18+
},
19+
}
20+
})
721

822
const writeKey = TEST_WRITEKEY
923

1024
describe('queryString', () => {
1125
let jsd: JSDOM
1226

1327
beforeEach(async () => {
14-
jest.restoreAllMocks()
15-
jest.resetAllMocks()
16-
1728
const html = `
1829
<!DOCTYPE html>
1930
<head>
@@ -37,33 +48,41 @@ describe('queryString', () => {
3748
setGlobalCDNUrl(undefined as any)
3849
})
3950

40-
it('applies query string logic before analytics is finished initializing', async () => {
41-
let analyticsInitializedBeforeQs: boolean | undefined
42-
const originalQueryString = Analytics.prototype.queryString
43-
const mockQueryString = jest
44-
.fn()
45-
.mockImplementation(async function (this: Analytics, ...args) {
46-
// simulate network latency when retrieving the bundle
47-
await new Promise((r) => setTimeout(r, 500))
48-
return originalQueryString.apply(this, args).then((result) => {
49-
// ensure analytics has not finished initializing before querystring completes
50-
analyticsInitializedBeforeQs = this.initialized
51-
return result
52-
})
53-
})
54-
Analytics.prototype.queryString = mockQueryString
51+
it('querystring events that update anonymousId have priority over other buffered events', async () => {
52+
const queryStringSpy = jest.spyOn(Analytics.prototype, 'queryString')
5553

5654
jsd.reconfigure({
5755
url: 'https://localhost/?ajs_aid=123',
5856
})
5957

60-
const [analytics] = await AnalyticsBrowser.load({ writeKey })
61-
expect(mockQueryString).toHaveBeenCalledWith('?ajs_aid=123')
62-
expect(analyticsInitializedBeforeQs).toBe(false)
63-
// check that calls made immediately after analytics is loaded use correct anonymousId
64-
const pageContext = await analytics.page()
58+
const analytics = new AnalyticsBrowser()
59+
const pagePromise = analytics.page()
60+
await analytics.load({ writeKey })
61+
expect(queryStringSpy).toHaveBeenCalledWith('?ajs_aid=123')
62+
const pageContext = await pagePromise
6563
expect(pageContext.event.anonymousId).toBe('123')
66-
expect(analytics.user().anonymousId()).toBe('123')
64+
const user = await analytics.user()
65+
expect(user.anonymousId()).toBe('123')
66+
})
67+
68+
it('querystring events have middleware applied like any other event', async () => {
69+
jsd.reconfigure({
70+
url: 'https://localhost/?ajs_event=Clicked',
71+
})
72+
73+
const analytics = new AnalyticsBrowser()
74+
void analytics.addSourceMiddleware(({ next, payload }) => {
75+
payload.obj.event = payload.obj.event + ' Middleware Applied'
76+
return next(payload)
77+
})
78+
await analytics.load({ writeKey })
79+
const trackCalls = fetchCalls.filter(
80+
(call) => call.url === 'https://api.segment.io/v1/t'
81+
)
82+
expect(trackCalls.length).toBe(1)
83+
expect(trackCalls[0].body.event).toMatchInlineSnapshot(
84+
`"Clicked Middleware Applied"`
85+
)
6786
})
6887

6988
it('applies query string logic if window.location.search is present', async () => {

packages/browser/src/browser/index.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,29 @@ function flushPreBuffer(
208208
*/
209209
async function flushFinalBuffer(
210210
analytics: Analytics,
211+
queryString: string,
211212
buffer: PreInitMethodCallBuffer
212213
): Promise<void> {
213-
// Call popSnippetWindowBuffer before each flush task since there may be
214-
// analytics calls during async function calls.
215-
await flushAddSourceMiddleware(analytics, buffer)
214+
await flushQueryString(analytics, queryString)
216215
flushAnalyticsCallsInNewTask(analytics, buffer)
217216
}
218217

218+
const getQueryString = (): string => {
219+
const hash = window.location.hash ?? ''
220+
const search = window.location.search ?? ''
221+
const term = search.length ? search : hash.replace(/(?=#).*(?=\?)/, '')
222+
return term
223+
}
224+
225+
const flushQueryString = async (
226+
analytics: Analytics,
227+
queryString: string
228+
): Promise<void> => {
229+
if (queryString.includes('ajs_')) {
230+
await analytics.queryString(queryString).catch(console.error)
231+
}
232+
}
233+
219234
async function registerPlugins(
220235
writeKey: string,
221236
cdnSettings: CDNSettings,
@@ -337,6 +352,9 @@ async function registerPlugins(
337352
})
338353
}
339354

355+
// register any user-defined plugins added via analytics.addSourceMiddleware()
356+
await flushAddSourceMiddleware(analytics, preInitBuffer)
357+
340358
return ctx
341359
}
342360

@@ -360,6 +378,9 @@ async function loadAnalytics(
360378
preInitBuffer.add(new PreInitMethodCall('page', []))
361379
}
362380

381+
// reading the query string as early as possible in case the URL changes
382+
const queryString = getQueryString()
383+
363384
const cdnURL = settings.cdnURL ?? getCDN()
364385
let cdnSettings =
365386
settings.cdnSettings ?? (await loadCDNSettings(settings.writeKey, cdnURL))
@@ -412,19 +433,9 @@ async function loadAnalytics(
412433
preInitBuffer
413434
)
414435

415-
const search = window.location.search ?? ''
416-
const hash = window.location.hash ?? ''
417-
418-
const term = search.length ? search : hash.replace(/(?=#).*(?=\?)/, '')
419-
420-
if (term.includes('ajs_')) {
421-
await analytics.queryString(term).catch(console.error)
422-
}
423-
424436
analytics.initialized = true
425437
analytics.emit('initialize', settings, options)
426-
427-
await flushFinalBuffer(analytics, preInitBuffer)
438+
await flushFinalBuffer(analytics, queryString, preInitBuffer)
428439

429440
return [analytics, ctx]
430441
}

turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"lint": {
4646
"dependsOn": ["build"],
47-
"inputs": ["**/tsconfig*.json", "**/*.ts", "**/*.tsx", "**/*.js"],
47+
"inputs": ["**/tsconfig*.json", "**/*.ts", "**/*.tsx"],
4848
"outputs": []
4949
},
5050
"test:cloudflare-workers": {

0 commit comments

Comments
 (0)