Skip to content

Commit 69154c3

Browse files
committed
Add useQueryString to InitOptions
1 parent d2d0459 commit 69154c3

File tree

6 files changed

+147
-4
lines changed

6 files changed

+147
-4
lines changed

.changeset/fuzzy-baboons-shave.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@segment/analytics-next': minor
3+
'@segment/analytics-core': patch
4+
---
5+
6+
Add useQueryString option to InitOptions

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ export interface InitOptions {
9393
plan?: Plan
9494
retryQueue?: boolean
9595
obfuscate?: boolean
96+
/**
97+
* Disables or sets constraints on processing of query string parameters
98+
*/
99+
useQueryString?:
100+
| boolean
101+
| {
102+
aid?: RegExp
103+
uid?: RegExp
104+
}
96105
}
97106

98107
/* analytics-classic stubs */
@@ -421,7 +430,11 @@ export class Analytics
421430
return this._user.anonymousId(id)
422431
}
423432

424-
async queryString(query: string): Promise<Context[]> {
433+
async queryString(query: string): Promise<Context[] | void> {
434+
if (this.options.useQueryString === false) {
435+
return
436+
}
437+
425438
const { queryString } = await import(
426439
/* webpackChunkName: "queryString" */ '../query-string'
427440
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import unfetch from 'unfetch'
2+
import { AnalyticsBrowser } from '../../..'
3+
import { createSuccess } from '../../../test-helpers/factories'
4+
5+
jest.mock('unfetch')
6+
jest
7+
.mocked(unfetch)
8+
.mockImplementation(() => createSuccess({ integrations: {} }))
9+
10+
// @ts-ignore
11+
delete window.location
12+
// @ts-ignore
13+
window.location = new URL(
14+
'https://www.example.com?ajs_aid=873832VB&ajs_uid=xcvn7568'
15+
)
16+
17+
describe('useQueryString configuration option', () => {
18+
it('ignores aid and uid from query string when disabled', async () => {
19+
const [analyticsAlt] = await AnalyticsBrowser.load(
20+
{ writeKey: 'abc' },
21+
{
22+
useQueryString: false,
23+
}
24+
)
25+
26+
// not acknowledge the aid provided in the query params, let ajs generate one
27+
expect(analyticsAlt.user().anonymousId()).not.toBe('873832VB')
28+
expect(analyticsAlt.user().id()).toBe(null)
29+
})
30+
31+
it('ignores uid when it doesnt match the required pattern', async () => {
32+
const [analyticsAlt] = await AnalyticsBrowser.load(
33+
{ writeKey: 'abc' },
34+
{
35+
useQueryString: {
36+
uid: /[A-Z]{6}/,
37+
},
38+
}
39+
)
40+
41+
// no constraint was set for aid therefore accepted
42+
expect(analyticsAlt.user().anonymousId()).toBe('873832VB')
43+
expect(analyticsAlt.user().id()).toBe(null)
44+
})
45+
46+
it('accepts both aid and uid from query string when they match the required pattern', async () => {
47+
const [analyticsAlt] = await AnalyticsBrowser.load(
48+
{ writeKey: 'abc' },
49+
{
50+
useQueryString: {
51+
aid: /\d{6}[A-Z]{2}/,
52+
uid: /[a-z]{4}\d{4}/,
53+
},
54+
}
55+
)
56+
57+
expect(analyticsAlt.user().anonymousId()).toBe('873832VB')
58+
expect(analyticsAlt.user().id()).toBe('xcvn7568')
59+
})
60+
})

packages/browser/src/core/query-string/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { pickPrefix } from './pickPrefix'
22
import { gracefulDecodeURIComponent } from './gracefulDecodeURIComponent'
33
import { Analytics } from '../analytics'
44
import { Context } from '../context'
5+
import { isPlainObject } from '@segment/analytics-core'
56

67
export interface QueryStringParams {
78
[key: string]: string | null
@@ -23,22 +24,32 @@ export function queryString(
2324
const calls = []
2425

2526
const { ajs_uid, ajs_event, ajs_aid } = params
27+
const { aid: aidPattern = /.+/, uid: uidPattern = /.+/ } = isPlainObject(
28+
analytics.options.useQueryString
29+
)
30+
? analytics.options.useQueryString
31+
: {}
2632

2733
if (ajs_aid) {
2834
const anonId = Array.isArray(params.ajs_aid)
2935
? params.ajs_aid[0]
3036
: params.ajs_aid
3137

32-
analytics.setAnonymousId(anonId)
38+
if (aidPattern.test(anonId)) {
39+
analytics.setAnonymousId(anonId)
40+
}
3341
}
3442

3543
if (ajs_uid) {
3644
const uid = Array.isArray(params.ajs_uid)
3745
? params.ajs_uid[0]
3846
: params.ajs_uid
39-
const traits = pickPrefix('ajs_trait_', params)
4047

41-
calls.push(analytics.identify(uid, traits))
48+
if (uidPattern.test(uid)) {
49+
const traits = pickPrefix('ajs_trait_', params)
50+
51+
calls.push(analytics.identify(uid, traits))
52+
}
4253
}
4354

4455
if (ajs_event) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Spec derived from https://github.com/jonschlinkert/is-plain-object/blob/master/test/server.js
2+
3+
import { isPlainObject } from '../is-plain-object'
4+
5+
describe('isPlainObject', () => {
6+
it('should return `true` if the object is created by the `Object` constructor.', function () {
7+
expect(isPlainObject(Object.create({}))).toBe(true)
8+
expect(isPlainObject(Object.create(Object.prototype))).toBe(true)
9+
expect(isPlainObject({ foo: 'bar' })).toBe(true)
10+
expect(isPlainObject({})).toBe(true)
11+
expect(isPlainObject(Object.create(null))).toBe(true)
12+
})
13+
14+
it('should return `false` if the object is not created by the `Object` constructor.', function () {
15+
class Foo {
16+
abc = {}
17+
}
18+
19+
expect(isPlainObject(/foo/)).toBe(false)
20+
expect(isPlainObject(function () {})).toBe(false)
21+
expect(isPlainObject(1)).toBe(false)
22+
expect(isPlainObject(['foo', 'bar'])).toBe(false)
23+
expect(isPlainObject([])).toBe(false)
24+
expect(isPlainObject(new Foo())).toBe(false)
25+
expect(isPlainObject(null)).toBe(false)
26+
})
27+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Code derived from https://github.com/jonschlinkert/is-plain-object/blob/master/is-plain-object.js
2+
3+
function isObject(o: unknown): o is Object {
4+
return Object.prototype.toString.call(o) === '[object Object]'
5+
}
6+
7+
export function isPlainObject(o: unknown): o is Record<PropertyKey, unknown> {
8+
if (isObject(o) === false) return false
9+
10+
// If has modified constructor
11+
const ctor = (o as any).constructor
12+
if (ctor === undefined) return true
13+
14+
// If has modified prototype
15+
const prot = ctor.prototype
16+
if (isObject(prot) === false) return false
17+
18+
// If constructor does not have an Object-specific method
19+
// eslint-disable-next-line no-prototype-builtins
20+
if ((prot as Object).hasOwnProperty('isPrototypeOf') === false) {
21+
return false
22+
}
23+
24+
// Most likely a plain Object
25+
return true
26+
}

0 commit comments

Comments
 (0)