Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fuzzy-baboons-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-next': minor
'@segment/analytics-core': patch
---

Add useQueryString option to InitOptions
13 changes: 13 additions & 0 deletions packages/browser/src/core/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ export interface InitOptions {
plan?: Plan
retryQueue?: boolean
obfuscate?: boolean
/**
* Disables or sets constraints on processing of query string parameters
*/
useQueryString?:
| boolean
| {
aid?: RegExp
uid?: RegExp
}
}

/* analytics-classic stubs */
Expand Down Expand Up @@ -422,6 +431,10 @@ export class Analytics
}

async queryString(query: string): Promise<Context[]> {
if (this.options.useQueryString === false) {
Copy link
Copy Markdown
Contributor

@silesky silesky Jan 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: should we return empty array instead to preserve the function signature?

Since .queryString is a public API and the function signature changed, this could be considered 'in theory' a breaking change. It would mostly affect existing typescript users who would need to update their code to handle the void case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Let me fix that

return []
}

const { queryString } = await import(
/* webpackChunkName: "queryString" */ '../query-string'
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import unfetch from 'unfetch'
import { AnalyticsBrowser } from '../../..'
import { createSuccess } from '../../../test-helpers/factories'

jest.mock('unfetch')
jest
.mocked(unfetch)
.mockImplementation(() => createSuccess({ integrations: {} }))

// @ts-ignore
delete window.location
// @ts-ignore
window.location = new URL(
'https://www.example.com?ajs_aid=873832VB&ajs_uid=xcvn7568'
)

describe('useQueryString configuration option', () => {
it('ignores aid and uid from query string when disabled', async () => {
const [analyticsAlt] = await AnalyticsBrowser.load(
{ writeKey: 'abc' },
{
useQueryString: false,
}
)

// not acknowledge the aid provided in the query params, let ajs generate one
expect(analyticsAlt.user().anonymousId()).not.toBe('873832VB')
expect(analyticsAlt.user().id()).toBe(null)
})

it('ignores uid when it doesnt match the required pattern', async () => {
const [analyticsAlt] = await AnalyticsBrowser.load(
{ writeKey: 'abc' },
{
useQueryString: {
uid: /[A-Z]{6}/,
},
}
)

// no constraint was set for aid therefore accepted
expect(analyticsAlt.user().anonymousId()).toBe('873832VB')
expect(analyticsAlt.user().id()).toBe(null)
})

it('accepts both aid and uid from query string when they match the required pattern', async () => {
const [analyticsAlt] = await AnalyticsBrowser.load(
{ writeKey: 'abc' },
{
useQueryString: {
aid: /\d{6}[A-Z]{2}/,
uid: /[a-z]{4}\d{4}/,
},
}
)

expect(analyticsAlt.user().anonymousId()).toBe('873832VB')
expect(analyticsAlt.user().id()).toBe('xcvn7568')
})
})
17 changes: 14 additions & 3 deletions packages/browser/src/core/query-string/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { pickPrefix } from './pickPrefix'
import { gracefulDecodeURIComponent } from './gracefulDecodeURIComponent'
import { Analytics } from '../analytics'
import { Context } from '../context'
import { isPlainObject } from '@segment/analytics-core'

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

const { ajs_uid, ajs_event, ajs_aid } = params
const { aid: aidPattern = /.+/, uid: uidPattern = /.+/ } = isPlainObject(
analytics.options.useQueryString
)
? analytics.options.useQueryString
: {}

if (ajs_aid) {
const anonId = Array.isArray(params.ajs_aid)
? params.ajs_aid[0]
: params.ajs_aid

analytics.setAnonymousId(anonId)
if (aidPattern.test(anonId)) {
analytics.setAnonymousId(anonId)
}
}

if (ajs_uid) {
const uid = Array.isArray(params.ajs_uid)
? params.ajs_uid[0]
: params.ajs_uid
const traits = pickPrefix('ajs_trait_', params)

calls.push(analytics.identify(uid, traits))
if (uidPattern.test(uid)) {
const traits = pickPrefix('ajs_trait_', params)

calls.push(analytics.identify(uid, traits))
}
}

if (ajs_event) {
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/utils/__tests__/is-plain-object.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Spec derived from https://github.com/jonschlinkert/is-plain-object/blob/master/test/server.js

import { isPlainObject } from '../is-plain-object'

describe('isPlainObject', () => {
it('should return `true` if the object is created by the `Object` constructor.', function () {
expect(isPlainObject(Object.create({}))).toBe(true)
expect(isPlainObject(Object.create(Object.prototype))).toBe(true)
expect(isPlainObject({ foo: 'bar' })).toBe(true)
expect(isPlainObject({})).toBe(true)
expect(isPlainObject(Object.create(null))).toBe(true)
})

it('should return `false` if the object is not created by the `Object` constructor.', function () {
class Foo {
abc = {}
}

expect(isPlainObject(/foo/)).toBe(false)
expect(isPlainObject(function () {})).toBe(false)
expect(isPlainObject(1)).toBe(false)
expect(isPlainObject(['foo', 'bar'])).toBe(false)
expect(isPlainObject([])).toBe(false)
expect(isPlainObject(new Foo())).toBe(false)
expect(isPlainObject(null)).toBe(false)
})
})
26 changes: 26 additions & 0 deletions packages/core/src/utils/is-plain-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Code derived from https://github.com/jonschlinkert/is-plain-object/blob/master/is-plain-object.js

function isObject(o: unknown): o is Object {
return Object.prototype.toString.call(o) === '[object Object]'
}

export function isPlainObject(o: unknown): o is Record<PropertyKey, unknown> {
if (isObject(o) === false) return false

// If has modified constructor
const ctor = (o as any).constructor
if (ctor === undefined) return true

// If has modified prototype
const prot = ctor.prototype
if (isObject(prot) === false) return false

// If constructor does not have an Object-specific method
// eslint-disable-next-line no-prototype-builtins
if ((prot as Object).hasOwnProperty('isPrototypeOf') === false) {
return false
}

// Most likely a plain Object
return true
}