Skip to content

Commit fdc004b

Browse files
authored
Fix wrong utm parsing by making context.page.search the source of truth for query params. (#839)
1 parent 55a48a0 commit fdc004b

File tree

3 files changed

+49
-48
lines changed

3 files changed

+49
-48
lines changed

.changeset/red-ears-sleep.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@segment/analytics-next': patch
3+
---
4+
5+
Fixes a utm-parameter parsing bug where overridden page.search properties would not be reflected in the context.campaign object
6+
```ts
7+
analytics.page(undefined, undefined, {search: "?utm_source=123&utm_content=content" )
8+
analytics.track("foo", {url: "....", search: "?utm_source=123&utm_content=content" )
9+
10+
// should result in a context.campaign of:
11+
{ source: 123, content: 'content'}
12+
```

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

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,24 @@ describe('before loading', () => {
5151
window.localStorage.clear()
5252
}
5353
})
54+
5455
describe('#normalize', () => {
5556
let object: SegmentEvent
57+
let defaultCtx: any
58+
const withSearchParams = (search?: string) => {
59+
object.context = { page: { search } }
60+
}
5661

5762
beforeEach(() => {
5863
cookie.remove('s:context.referrer')
64+
defaultCtx = {
65+
page: {
66+
search: '',
67+
},
68+
}
5969
object = {
6070
type: 'track',
71+
context: defaultCtx,
6172
}
6273
})
6374

@@ -94,17 +105,15 @@ describe('before loading', () => {
94105
})
95106

96107
it('should always add .anonymousId even if .userId is given', () => {
97-
const object: SegmentEvent = { userId: 'baz', type: 'track' }
108+
object.userId = 'baz'
98109
normalize(analytics, object, options, {})
99110
assert(object.anonymousId?.length === 36)
100111
})
101112

102113
it('should accept anonymousId being set in an event', async () => {
103-
const object: SegmentEvent = {
104-
userId: 'baz',
105-
type: 'track',
106-
anonymousId: '👻',
107-
}
114+
object.userId = 'baz'
115+
object.anonymousId = '👻'
116+
108117
normalize(analytics, object, options, {})
109118
expect(object.anonymousId).toEqual('👻')
110119
})
@@ -115,15 +124,16 @@ describe('before loading', () => {
115124
})
116125

117126
it('should not rewrite context if provided', () => {
118-
const ctx = {}
127+
const ctx = defaultCtx
119128
const obj = { ...object, context: ctx }
120129
normalize(analytics, obj, options, {})
121130
expect(obj.context).toEqual(ctx)
122131
})
123132

124-
it('should copy .options to .context', () => {
133+
it('should overwrite options with context if context does not exist', () => {
125134
const opts = {}
126135
const obj = { ...object, options: opts }
136+
delete obj.context
127137
normalize(analytics, obj, options, {})
128138
assert(obj.context === opts)
129139
assert(obj.options == null)
@@ -172,6 +182,7 @@ describe('before loading', () => {
172182

173183
it('should not replace .locale if provided', () => {
174184
const ctx = {
185+
...defaultCtx,
175186
locale: 'foobar',
176187
}
177188
const obj = { ...object, context: ctx }
@@ -180,10 +191,9 @@ describe('before loading', () => {
180191
})
181192

182193
it('should add .campaign', () => {
183-
jsdom.reconfigure({
184-
url: 'http://localhost?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name',
185-
})
186-
194+
withSearchParams(
195+
'utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'
196+
)
187197
normalize(analytics, object, options, {})
188198

189199
assert(object)
@@ -197,10 +207,7 @@ describe('before loading', () => {
197207
})
198208

199209
it('should decode query params', () => {
200-
jsdom.reconfigure({
201-
url: 'http://localhost?utm_source=%5BFoo%5D',
202-
})
203-
210+
withSearchParams('?utm_source=%5BFoo%5D')
204211
normalize(analytics, object, options, {})
205212

206213
assert(object)
@@ -210,9 +217,7 @@ describe('before loading', () => {
210217
})
211218

212219
it('should guard against undefined utm params', () => {
213-
jsdom.reconfigure({
214-
url: 'http://localhost?utm_source',
215-
})
220+
withSearchParams('?utm_source')
216221

217222
normalize(analytics, object, options, {})
218223

@@ -223,10 +228,7 @@ describe('before loading', () => {
223228
})
224229

225230
it('should guard against empty utm params', () => {
226-
jsdom.reconfigure({
227-
url: 'http://localhost?utm_source=',
228-
})
229-
231+
withSearchParams('?utm_source=')
230232
normalize(analytics, object, options, {})
231233

232234
assert(object)
@@ -236,10 +238,7 @@ describe('before loading', () => {
236238
})
237239

238240
it('only parses utm params suffixed with _', () => {
239-
jsdom.reconfigure({
240-
url: 'http://localhost?utm',
241-
})
242-
241+
withSearchParams('?utm')
243242
normalize(analytics, object, options, {})
244243

245244
assert(object)
@@ -248,9 +247,7 @@ describe('before loading', () => {
248247
})
249248

250249
it('should guard against short utm params', () => {
251-
jsdom.reconfigure({
252-
url: 'http://localhost?utm_',
253-
})
250+
withSearchParams('?utm_')
254251

255252
normalize(analytics, object, options, {})
256253

@@ -260,13 +257,14 @@ describe('before loading', () => {
260257
})
261258

262259
it('should allow override of .campaign', () => {
263-
jsdom.reconfigure({
264-
url: 'http://localhost?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name',
265-
})
260+
withSearchParams(
261+
'?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'
262+
)
266263

267264
const obj = {
268265
...object,
269266
context: {
267+
...defaultCtx,
270268
campaign: {
271269
source: 'overrideSource',
272270
medium: 'overrideMedium',
@@ -288,9 +286,7 @@ describe('before loading', () => {
288286
})
289287

290288
it('should add .referrer.id and .referrer.type (cookies)', () => {
291-
jsdom.reconfigure({
292-
url: 'http://localhost?utm_source=source&urid=medium',
293-
})
289+
withSearchParams('?utm_source=source&urid=medium')
294290

295291
normalize(analytics, object, options, {})
296292
assert(object)
@@ -307,9 +303,7 @@ describe('before loading', () => {
307303
})
308304

309305
it('should add .referrer.id and .referrer.type (cookieless)', () => {
310-
jsdom.reconfigure({
311-
url: 'http://localhost?utm_source=source&urid=medium',
312-
})
306+
withSearchParams('utm_source=source&urid=medium')
313307
const setCookieSpy = jest.spyOn(cookie, 'set')
314308
analytics = new Analytics(
315309
{ writeKey: options.apiKey },
@@ -329,10 +323,6 @@ describe('before loading', () => {
329323
it('should add .referrer.id and .referrer.type from cookie', () => {
330324
cookie.set('s:context.referrer', '{"id":"baz","type":"millennial-media"}')
331325

332-
jsdom.reconfigure({
333-
url: 'http://localhost?utm_source=source',
334-
})
335-
336326
normalize(analytics, object, options, {})
337327

338328
assert(object)
@@ -348,10 +338,6 @@ describe('before loading', () => {
348338
'{"id":"medium","type":"millennial-media"}'
349339
)
350340

351-
jsdom.reconfigure({
352-
url: 'http://localhost',
353-
})
354-
355341
normalize(analytics, object, options, {})
356342
assert(object)
357343
assert(object.context)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,14 @@ export function normalize(
125125
integrations?: LegacySettings['integrations']
126126
): object {
127127
const user = analytics.user()
128-
const query = window.location.search
129128

129+
// context should always exist here (see page enrichment)? ... and why would we default to json.options? todo: delete this
130130
json.context = json.context ?? json.options ?? {}
131131
const ctx = json.context
132132

133+
// This guard against missing ctx.page should not be neccessary, since context.page is always defined
134+
const query: string = ctx.page?.search || ''
135+
133136
delete json.options
134137
json.writeKey = settings?.apiKey
135138
ctx.userAgent = window.navigator.userAgent

0 commit comments

Comments
 (0)