Skip to content

Commit a72f473

Browse files
authored
Add disable API boolean (#992)
1 parent dcf279c commit a72f473

File tree

7 files changed

+144
-4
lines changed

7 files changed

+144
-4
lines changed

.changeset/unlucky-kids-invite.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+
Add 'disable' boolean option to allow for disabling Segment in a testing environment.

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"size-limit": [
4545
{
4646
"path": "dist/umd/index.js",
47-
"limit": "29.2 KB"
47+
"limit": "29.5 KB"
4848
}
4949
],
5050
"dependencies": {

packages/browser/src/browser/__tests__/integration.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
highEntropyTestData,
2424
lowEntropyTestData,
2525
} from '../../test-helpers/fixtures/client-hints'
26-
import { getGlobalAnalytics } from '../..'
26+
import { getGlobalAnalytics, NullAnalytics } from '../..'
2727

2828
let fetchCalls: ReturnType<typeof parseFetchCall>[] = []
2929

@@ -94,11 +94,11 @@ const amplitudeWriteKey = 'bar'
9494

9595
beforeEach(() => {
9696
setGlobalCDNUrl(undefined as any)
97+
fetchCalls = []
9798
})
9899

99100
describe('Initialization', () => {
100101
beforeEach(async () => {
101-
fetchCalls = []
102102
jest.resetAllMocks()
103103
jest.resetModules()
104104
})
@@ -1209,4 +1209,56 @@ describe('Options', () => {
12091209
expect(integrationEvent.timestamp()).toBeInstanceOf(Date)
12101210
})
12111211
})
1212+
1213+
describe('disable', () => {
1214+
/**
1215+
* Note: other tests in null-analytics.test.ts cover the NullAnalytics class (including persistence)
1216+
*/
1217+
it('should return a null version of analytics / context', async () => {
1218+
const [analytics, context] = await AnalyticsBrowser.load(
1219+
{
1220+
writeKey,
1221+
},
1222+
{ disable: true }
1223+
)
1224+
expect(context).toBeInstanceOf(Context)
1225+
expect(analytics).toBeInstanceOf(NullAnalytics)
1226+
expect(analytics.initialized).toBe(true)
1227+
})
1228+
1229+
it('should not fetch cdn settings or dispatch events', async () => {
1230+
const [analytics] = await AnalyticsBrowser.load(
1231+
{
1232+
writeKey,
1233+
},
1234+
{ disable: true }
1235+
)
1236+
await analytics.track('foo')
1237+
expect(fetchCalls.length).toBe(0)
1238+
})
1239+
1240+
it('should only accept a boolean value', async () => {
1241+
const [analytics] = await AnalyticsBrowser.load(
1242+
{
1243+
writeKey,
1244+
},
1245+
// @ts-ignore
1246+
{ disable: 'true' }
1247+
)
1248+
expect(analytics).not.toBeInstanceOf(NullAnalytics)
1249+
})
1250+
1251+
it('should allow access to cdnSettings', async () => {
1252+
const disableSpy = jest.fn().mockReturnValue(true)
1253+
const [analytics] = await AnalyticsBrowser.load(
1254+
{
1255+
cdnSettings: { integrations: {}, foo: 123 },
1256+
writeKey,
1257+
},
1258+
{ disable: disableSpy }
1259+
)
1260+
expect(analytics).toBeInstanceOf(NullAnalytics)
1261+
expect(disableSpy).toBeCalledWith({ integrations: {}, foo: 123 })
1262+
})
1263+
})
12121264
})

packages/browser/src/browser/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { getProcessEnv } from '../lib/get-process-env'
22
import { getCDN, setGlobalCDNUrl } from '../lib/parse-cdn'
33

44
import { fetch } from '../lib/fetch'
5-
import { Analytics, AnalyticsSettings, InitOptions } from '../core/analytics'
5+
import {
6+
Analytics,
7+
AnalyticsSettings,
8+
NullAnalytics,
9+
InitOptions,
10+
} from '../core/analytics'
611
import { Context } from '../core/context'
712
import { Plan } from '../core/events'
813
import { Plugin } from '../core/plugin'
@@ -300,6 +305,11 @@ async function loadAnalytics(
300305
options: InitOptions = {},
301306
preInitBuffer: PreInitMethodCallBuffer
302307
): Promise<[Analytics, Context]> {
308+
// return no-op analytics instance if disabled
309+
if (options.disable === true) {
310+
return [new NullAnalytics(), Context.system()]
311+
}
312+
303313
if (options.globalAnalyticsKey)
304314
setGlobalAnalyticsKey(options.globalAnalyticsKey)
305315
// this is an ugly side-effect, but it's for the benefits of the plugins that get their cdn via getCDN()
@@ -313,6 +323,14 @@ async function loadAnalytics(
313323
legacySettings = options.updateCDNSettings(legacySettings)
314324
}
315325

326+
// if options.disable is a function, we allow user to disable analytics based on CDN Settings
327+
if (typeof options.disable === 'function') {
328+
const disabled = await options.disable(legacySettings)
329+
if (disabled) {
330+
return [new NullAnalytics(), Context.system()]
331+
}
332+
}
333+
316334
const retryQueue: boolean =
317335
legacySettings.integrations['Segment.io']?.retryQueue ?? true
318336

packages/browser/src/core/analytics/__tests__/integration.test.ts renamed to packages/browser/src/core/analytics/__tests__/analytics.test.ts

File renamed without changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { getAjsBrowserStorage } from '../../../test-helpers/browser-storage'
2+
import { Analytics, NullAnalytics } from '..'
3+
4+
describe(NullAnalytics, () => {
5+
it('should return an instance of Analytics / NullAnalytics', () => {
6+
const analytics = new NullAnalytics()
7+
expect(analytics).toBeInstanceOf(Analytics)
8+
expect(analytics).toBeInstanceOf(NullAnalytics)
9+
})
10+
11+
it('should have initialized set to true', () => {
12+
const analytics = new NullAnalytics()
13+
expect(analytics.initialized).toBe(true)
14+
})
15+
16+
it('should have no plugins', async () => {
17+
const analytics = new NullAnalytics()
18+
expect(analytics.queue.plugins).toHaveLength(0)
19+
})
20+
it('should dispatch events', async () => {
21+
const analytics = new NullAnalytics()
22+
const ctx = await analytics.track('foo')
23+
expect(ctx.event.event).toBe('foo')
24+
})
25+
26+
it('should have disableClientPersistence set to true', () => {
27+
const analytics = new NullAnalytics()
28+
expect(analytics.options.disableClientPersistence).toBe(true)
29+
})
30+
31+
it('integration: should not touch cookies or localStorage', async () => {
32+
const analytics = new NullAnalytics()
33+
await analytics.track('foo')
34+
const storage = getAjsBrowserStorage()
35+
expect(Object.values(storage).every((v) => !v)).toBe(true)
36+
})
37+
})

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ export interface InitOptions {
129129
* default: analytics
130130
*/
131131
globalAnalyticsKey?: string
132+
133+
/**
134+
* Disable sending any data to Segment's servers. All emitted events and API calls (including .ready()), will be no-ops, and no cookies or localstorage will be used.
135+
*
136+
* @example
137+
* ### Basic (Will not not fetch any CDN settings)
138+
* ```ts
139+
* disable: process.env.NODE_ENV === 'test'
140+
* ```
141+
*
142+
* ### Advanced (Fetches CDN Settings. Do not use this unless you require CDN settings for some reason)
143+
* ```ts
144+
* disable: (cdnSettings) => cdnSettings.foo === 'bar'
145+
* ```
146+
*/
147+
disable?:
148+
| boolean
149+
| ((cdnSettings: LegacySettings) => boolean | Promise<boolean>)
132150
}
133151

134152
/* analytics-classic stubs */
@@ -652,3 +670,13 @@ export class Analytics
652670
an[method].apply(this, args)
653671
}
654672
}
673+
674+
/**
675+
* @returns a no-op analytics instance that does not create cookies or localstorage, or send any events to segment.
676+
*/
677+
export class NullAnalytics extends Analytics {
678+
constructor() {
679+
super({ writeKey: '' }, { disableClientPersistence: true })
680+
this.initialized = true
681+
}
682+
}

0 commit comments

Comments
 (0)