@segment/analytics-browser-actions-amplitude-plugins 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@segment/analytics-browser-actions-amplitude-plugins",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "./dist/cjs",
6
+ "module": "./dist/esm",
7
+ "scripts": {
8
+ "build": "yarn build:esm && yarn build:cjs",
9
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
10
+ "build:esm": "tsc --outDir ./dist/esm"
11
+ },
12
+ "typings": "./dist/esm",
13
+ "dependencies": {
14
+ "@segment/browser-destination-runtime": "^1.0.0"
15
+ },
16
+ "peerDependencies": {
17
+ "@segment/analytics-next": "*"
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Settings {}
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Settings } from './generated-types'
2
+ import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
3
+ import { browserDestination } from '@segment/browser-destination-runtime/shim'
4
+ import sessionId from './sessionId'
5
+
6
+ export const destination: BrowserDestinationDefinition<Settings, {}> = {
7
+ name: 'Amplitude (Actions)',
8
+ mode: 'device',
9
+ actions: {
10
+ sessionId
11
+ },
12
+ initialize: async () => {
13
+ return {}
14
+ }
15
+ }
16
+
17
+ export default browserDestination(destination)
@@ -0,0 +1,358 @@
1
+ import { Analytics, Context, Plugin } from '@segment/analytics-next'
2
+ import browserPluginsDestination from '../..'
3
+ import { Subscription } from '@segment/browser-destination-runtime/types'
4
+ import jar from 'js-cookie'
5
+
6
+ expect.extend({
7
+ toBeWithinOneSecondOf(got, expected) {
8
+ if (typeof got === 'string') {
9
+ got = parseInt(got, 10)
10
+ }
11
+
12
+ if (typeof expected === 'string') {
13
+ expected = parseInt(expected, 10)
14
+ }
15
+
16
+ const oneSecond = 1000
17
+
18
+ const timeDiff = Math.abs(expected - got)
19
+ const timeDiffInSeconds = timeDiff / 1000
20
+
21
+ const pass = timeDiff < oneSecond
22
+ const message = () =>
23
+ `${got} should be within a second of ${expected}, ` + `actual difference: ${timeDiffInSeconds.toFixed(1)}s`
24
+
25
+ return { pass, message }
26
+ }
27
+ })
28
+
29
+ const example: Subscription[] = [
30
+ {
31
+ partnerAction: 'sessionId',
32
+ name: 'SessionId',
33
+ enabled: true,
34
+ subscribe: 'type = "track"',
35
+ mapping: {}
36
+ }
37
+ ]
38
+
39
+ let browserActions: Plugin[]
40
+ let sessionIdPlugin: Plugin
41
+ let ajs: Analytics
42
+
43
+ beforeEach(async () => {
44
+ browserActions = await browserPluginsDestination({ subscriptions: example })
45
+ sessionIdPlugin = browserActions[0]
46
+
47
+ // clear storage and cookies
48
+ document.cookie.split(';').forEach(function (c) {
49
+ document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/')
50
+ })
51
+ window.localStorage.clear()
52
+
53
+ ajs = new Analytics({
54
+ writeKey: 'w_123'
55
+ })
56
+ })
57
+
58
+ describe('ajs-integration', () => {
59
+ test('updates the original event with a session id', async () => {
60
+ await sessionIdPlugin.load(Context.system(), ajs)
61
+
62
+ const ctx = new Context({
63
+ type: 'track',
64
+ event: 'greet',
65
+ properties: {
66
+ greeting: 'Oi!'
67
+ }
68
+ })
69
+
70
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
71
+
72
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
73
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined()
74
+ // @ts-expect-error
75
+ expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number')
76
+ })
77
+
78
+ test('updates the original eveent when All: false but Actions Amplitude: true', async () => {
79
+ await sessionIdPlugin.load(Context.system(), ajs)
80
+
81
+ const ctx = new Context({
82
+ type: 'track',
83
+ event: 'greet',
84
+ properties: {
85
+ greeting: 'Oi!'
86
+ },
87
+ integrations: {
88
+ All: false,
89
+ 'Actions Amplitude': true
90
+ }
91
+ })
92
+
93
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
94
+
95
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
96
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).not.toBeUndefined()
97
+ // @ts-expect-error
98
+ expect(typeof updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBe('number')
99
+ })
100
+
101
+ test('doesnt update the original event with a session id when All: false', async () => {
102
+ await sessionIdPlugin.load(Context.system(), ajs)
103
+
104
+ const ctx = new Context({
105
+ type: 'track',
106
+ event: 'greet',
107
+ properties: {
108
+ greeting: 'Oi!'
109
+ },
110
+ integrations: {
111
+ All: false
112
+ }
113
+ })
114
+
115
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
116
+
117
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
118
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeUndefined()
119
+ })
120
+
121
+ test('runs as an enrichment middleware', async () => {
122
+ await ajs.register(sessionIdPlugin)
123
+ jest.spyOn(sessionIdPlugin, 'track')
124
+
125
+ const ctx = new Context({
126
+ type: 'track',
127
+ event: 'greet',
128
+ properties: {
129
+ greeting: 'Oi!'
130
+ }
131
+ })
132
+
133
+ await ajs.track(ctx.event)
134
+
135
+ expect(sessionIdPlugin.track).toHaveBeenCalled()
136
+ expect(ajs.queue.plugins.map((p) => ({ name: p.name, type: p.type }))).toMatchInlineSnapshot(`
137
+ Array [
138
+ Object {
139
+ "name": "Amplitude (Actions) sessionId",
140
+ "type": "enrichment",
141
+ },
142
+ ]
143
+ `)
144
+ })
145
+ })
146
+
147
+ describe('sessionId', () => {
148
+ beforeEach(async () => {
149
+ jest.useFakeTimers('legacy')
150
+ await sessionIdPlugin.load(Context.system(), ajs)
151
+ })
152
+
153
+ const id = () => new Date().getTime()
154
+
155
+ describe('new sessions', () => {
156
+ test('sets a session id', async () => {
157
+ const ctx = new Context({
158
+ type: 'track',
159
+ event: 'greet',
160
+ properties: {
161
+ greeting: 'Oi!'
162
+ }
163
+ })
164
+
165
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
166
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
167
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(id())
168
+ })
169
+
170
+ test('persists the session id', async () => {
171
+ const ctx = new Context({
172
+ type: 'track',
173
+ event: 'greet',
174
+ properties: {
175
+ greeting: 'Oi!'
176
+ }
177
+ })
178
+
179
+ await sessionIdPlugin.track?.(ctx)
180
+
181
+ // persists the session id in both cookies and local storage
182
+ expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(id().toString())
183
+ expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString())
184
+ expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(id().toString())
185
+ expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(id().toString())
186
+ expect(jar.get('analytics_session_id')).toBe(window.localStorage.getItem('analytics_session_id'))
187
+ expect(jar.get('analytics_session_id.last_access')).toBe(window.localStorage.getItem('analytics_session_id'))
188
+ })
189
+ })
190
+
191
+ describe('existing sessions', () => {
192
+ test('uses an existing session id in LS', async () => {
193
+ const then = id()
194
+ jest.advanceTimersByTime(10000)
195
+
196
+ window.localStorage.setItem('analytics_session_id', then.toString())
197
+
198
+ const ctx = new Context({
199
+ type: 'track',
200
+ event: 'greet',
201
+ properties: {
202
+ greeting: 'Oi!'
203
+ }
204
+ })
205
+
206
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
207
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
208
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then)
209
+ })
210
+
211
+ test('sync session info from LS to cookies', async () => {
212
+ const then = id()
213
+
214
+ window.localStorage.setItem('analytics_session_id', then.toString())
215
+ window.localStorage.setItem('analytics_session_id.last_access', then.toString())
216
+
217
+ const ctx = new Context({
218
+ type: 'track',
219
+ event: 'greet',
220
+ properties: {
221
+ greeting: 'Oi!'
222
+ }
223
+ })
224
+
225
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
226
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
227
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then)
228
+ expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(then)
229
+ })
230
+
231
+ test('uses an existing session id stored in cookies and sync it with local storage', async () => {
232
+ const then = id()
233
+ jest.advanceTimersByTime(10000)
234
+ jar.set('analytics_session_id', then.toString())
235
+
236
+ const ctx = new Context({
237
+ type: 'track',
238
+ event: 'greet',
239
+ properties: {
240
+ greeting: 'Oi!'
241
+ }
242
+ })
243
+ const now = id()
244
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
245
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
246
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then)
247
+ // synced to local storage
248
+ expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now)
249
+ expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(then)
250
+ })
251
+
252
+ test('keeps track of when the session was last accessed', async () => {
253
+ const then = id()
254
+ jest.advanceTimersByTime(10000)
255
+ window.localStorage.setItem('analytics_session_id', then.toString())
256
+
257
+ const now = id()
258
+
259
+ const ctx = new Context({
260
+ type: 'track',
261
+ event: 'greet',
262
+ properties: {
263
+ greeting: 'Oi!'
264
+ }
265
+ })
266
+
267
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
268
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
269
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then)
270
+
271
+ expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now)
272
+ expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now)
273
+ })
274
+
275
+ test('reset session when stale', async () => {
276
+ const then = id()
277
+ window.localStorage.setItem('analytics_session_id.last_access', then.toString())
278
+ window.localStorage.setItem('analytics_session_id', then.toString())
279
+
280
+ const THIRTY_MINUTES = 30 * 60000
281
+ jest.advanceTimersByTime(THIRTY_MINUTES)
282
+
283
+ const now = id()
284
+
285
+ const ctx = new Context({
286
+ type: 'track',
287
+ event: 'greet',
288
+ properties: {
289
+ greeting: 'Oi!'
290
+ }
291
+ })
292
+
293
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
294
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
295
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now)
296
+
297
+ expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString())
298
+ expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString())
299
+ expect(jar.get('analytics_session_id')).toBeWithinOneSecondOf(now.toString())
300
+ expect(jar.get('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString())
301
+ })
302
+ })
303
+
304
+ describe('work without AJS storage layer', () => {
305
+ test('uses an existing session id in LS when AJS storage layer is not available', async () => {
306
+ const then = id()
307
+ //@ts-expect-error
308
+ jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined)
309
+ jest.advanceTimersByTime(10000)
310
+
311
+ window.localStorage.setItem('analytics_session_id', then.toString())
312
+
313
+ const ctx = new Context({
314
+ type: 'track',
315
+ event: 'greet',
316
+ properties: {
317
+ greeting: 'Oi!'
318
+ }
319
+ })
320
+
321
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
322
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
323
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(then)
324
+ expect(jar.get('analytics_session_id')).toBe(undefined)
325
+ })
326
+
327
+ test('uses an existing session id in LS when AJS storage layer is not available', async () => {
328
+ const then = id()
329
+ //@ts-expect-error
330
+ jest.spyOn(ajs, 'storage', 'get').mockReturnValue(undefined)
331
+
332
+ window.localStorage.setItem('analytics_session_id.last_access', then.toString())
333
+ window.localStorage.setItem('analytics_session_id', then.toString())
334
+
335
+ const THIRTY_MINUTES = 30 * 60000
336
+ jest.advanceTimersByTime(THIRTY_MINUTES)
337
+
338
+ const now = id()
339
+
340
+ const ctx = new Context({
341
+ type: 'track',
342
+ event: 'greet',
343
+ properties: {
344
+ greeting: 'Oi!'
345
+ }
346
+ })
347
+
348
+ const updatedCtx = await sessionIdPlugin.track?.(ctx)
349
+ // @ts-expect-error Need to fix ajs-next types to allow for complex objects in `integrations`
350
+ expect(updatedCtx?.event.integrations['Actions Amplitude']?.session_id).toBeWithinOneSecondOf(now)
351
+
352
+ expect(window.localStorage.getItem('analytics_session_id')).toBeWithinOneSecondOf(now.toString())
353
+ expect(window.localStorage.getItem('analytics_session_id.last_access')).toBeWithinOneSecondOf(now.toString())
354
+ expect(jar.get('analytics_session_id')).toBeUndefined()
355
+ expect(jar.get('analytics_session_id.last_access')).toBeUndefined()
356
+ })
357
+ })
358
+ })
@@ -0,0 +1,8 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * Time in milliseconds to be used before considering a session stale.
6
+ */
7
+ sessionLength?: number
8
+ }
@@ -0,0 +1,88 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
2
+ import { UniversalStorage } from '@segment/analytics-next'
3
+ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
4
+ import type { Settings } from '../generated-types'
5
+ import type { Payload } from './generated-types'
6
+
7
+ function newSessionId(): number {
8
+ return now()
9
+ }
10
+
11
+ function now(): number {
12
+ return new Date().getTime()
13
+ }
14
+
15
+ const THIRTY_MINUTES = 30 * 60000
16
+
17
+ function stale(id: number | null, updated: number | null, length: number = THIRTY_MINUTES): id is null {
18
+ if (id === null || updated === null) {
19
+ return true
20
+ }
21
+
22
+ const accessedAt = updated
23
+
24
+ if (now() - accessedAt >= length) {
25
+ return true
26
+ }
27
+
28
+ return false
29
+ }
30
+
31
+ const action: BrowserActionDefinition<Settings, {}, Payload> = {
32
+ title: 'Session Plugin',
33
+ description: 'Generates a Session ID and attaches it to every Amplitude browser based event.',
34
+ platform: 'web',
35
+ hidden: true,
36
+ defaultSubscription: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"',
37
+ fields: {
38
+ sessionLength: {
39
+ label: 'Session Length',
40
+ type: 'number',
41
+ required: false,
42
+ description: 'Time in milliseconds to be used before considering a session stale.'
43
+ }
44
+ },
45
+ lifecycleHook: 'enrichment',
46
+ perform: (_, { context, payload, analytics }) => {
47
+ // TODO: this can be removed when storage layer in AJS is rolled out to all customers
48
+ const storageFallback = {
49
+ get: (key: string) => {
50
+ const data = window.localStorage.getItem(key)
51
+ return data === null ? null : parseInt(data, 10)
52
+ },
53
+ set: (key: string, value: number) => {
54
+ return window.localStorage.setItem(key, value.toString())
55
+ }
56
+ }
57
+
58
+ const newSession = newSessionId()
59
+ const storage = analytics.storage
60
+ ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
61
+ (analytics.storage as UniversalStorage<Record<string, number>>)
62
+ : storageFallback
63
+
64
+ const raw = storage.get('analytics_session_id')
65
+ const updated = storage.get('analytics_session_id.last_access')
66
+
67
+ let id: number | null = raw
68
+ if (stale(raw, updated, payload.sessionLength)) {
69
+ id = newSession
70
+ storage.set('analytics_session_id', id)
71
+ } else {
72
+ // we are storing the session id regardless, so it gets synced between different storage mediums
73
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- id can't be null because of stale check
74
+ storage.set('analytics_session_id', id!)
75
+ }
76
+
77
+ storage.set('analytics_session_id.last_access', newSession)
78
+
79
+ if (context.event.integrations?.All !== false || context.event.integrations['Actions Amplitude']) {
80
+ context.updateEvent('integrations.Actions Amplitude', {})
81
+ context.updateEvent('integrations.Actions Amplitude.session_id', id)
82
+ }
83
+
84
+ return
85
+ }
86
+ }
87
+
88
+ export default action
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "baseUrl": "."
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["dist", "**/__tests__"]
9
+ }