From 69154c31f0739c3d1e31c3fd4d0f075fac721289 Mon Sep 17 00:00:00 2001 From: Neek Sandhu Date: Wed, 25 Jan 2023 16:56:58 -0800 Subject: [PATCH] Add useQueryString to InitOptions --- .changeset/fuzzy-baboons-shave.md | 6 ++ packages/browser/src/core/analytics/index.ts | 15 ++++- .../__tests__/useQueryString.test.ts | 60 +++++++++++++++++++ .../browser/src/core/query-string/index.ts | 17 +++++- .../utils/__tests__/is-plain-object.test.ts | 27 +++++++++ packages/core/src/utils/is-plain-object.ts | 26 ++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 .changeset/fuzzy-baboons-shave.md create mode 100644 packages/browser/src/core/query-string/__tests__/useQueryString.test.ts create mode 100644 packages/core/src/utils/__tests__/is-plain-object.test.ts create mode 100644 packages/core/src/utils/is-plain-object.ts diff --git a/.changeset/fuzzy-baboons-shave.md b/.changeset/fuzzy-baboons-shave.md new file mode 100644 index 000000000..7c9a30db1 --- /dev/null +++ b/.changeset/fuzzy-baboons-shave.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-next': minor +'@segment/analytics-core': patch +--- + +Add useQueryString option to InitOptions diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index 46ec1ef66..019d228c6 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -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 */ @@ -421,7 +430,11 @@ export class Analytics return this._user.anonymousId(id) } - async queryString(query: string): Promise { + async queryString(query: string): Promise { + if (this.options.useQueryString === false) { + return + } + const { queryString } = await import( /* webpackChunkName: "queryString" */ '../query-string' ) diff --git a/packages/browser/src/core/query-string/__tests__/useQueryString.test.ts b/packages/browser/src/core/query-string/__tests__/useQueryString.test.ts new file mode 100644 index 000000000..2e5901156 --- /dev/null +++ b/packages/browser/src/core/query-string/__tests__/useQueryString.test.ts @@ -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') + }) +}) diff --git a/packages/browser/src/core/query-string/index.ts b/packages/browser/src/core/query-string/index.ts index b1c0b2f99..6e32e16d2 100644 --- a/packages/browser/src/core/query-string/index.ts +++ b/packages/browser/src/core/query-string/index.ts @@ -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 @@ -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) { diff --git a/packages/core/src/utils/__tests__/is-plain-object.test.ts b/packages/core/src/utils/__tests__/is-plain-object.test.ts new file mode 100644 index 000000000..55bd84714 --- /dev/null +++ b/packages/core/src/utils/__tests__/is-plain-object.test.ts @@ -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) + }) +}) diff --git a/packages/core/src/utils/is-plain-object.ts b/packages/core/src/utils/is-plain-object.ts new file mode 100644 index 000000000..9124c406f --- /dev/null +++ b/packages/core/src/utils/is-plain-object.ts @@ -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 { + 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 +}