@segment/analytics-browser-actions-heap 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 +20 -0
- package/src/__tests__/index.test.ts +43 -0
- package/src/constants.ts +1 -0
- package/src/generated-types.ts +28 -0
- package/src/identifyUser/__tests__/index.test.ts +78 -0
- package/src/identifyUser/generated-types.ts +14 -0
- package/src/identifyUser/index.ts +41 -0
- package/src/index.ts +118 -0
- package/src/test-utilities.ts +64 -0
- package/src/trackEvent/__tests__/index.test.ts +217 -0
- package/src/trackEvent/generated-types.ts +28 -0
- package/src/trackEvent/index.ts +123 -0
- package/src/types.ts +22 -0
- package/src/utils.ts +54 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@segment/analytics-browser-actions-heap",
|
|
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/actions-core": "^3.71.0",
|
|
15
|
+
"@segment/browser-destination-runtime": "^1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@segment/analytics-next": "*"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import heapDestination, { destination } from '../index'
|
|
3
|
+
import { HEAP_TEST_ENV_ID, mockHeapJsHttpRequest } from '../test-utilities'
|
|
4
|
+
|
|
5
|
+
const subscriptions = [
|
|
6
|
+
{
|
|
7
|
+
partnerAction: 'trackEvent',
|
|
8
|
+
name: 'Track Event',
|
|
9
|
+
enabled: true,
|
|
10
|
+
subscribe: 'type = "track"',
|
|
11
|
+
mapping: {}
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
describe('Heap', () => {
|
|
16
|
+
test('loading', async () => {
|
|
17
|
+
jest.spyOn(destination, 'initialize')
|
|
18
|
+
|
|
19
|
+
mockHeapJsHttpRequest()
|
|
20
|
+
|
|
21
|
+
const [event] = await heapDestination({ appId: HEAP_TEST_ENV_ID, subscriptions })
|
|
22
|
+
|
|
23
|
+
await event.load(Context.system(), {} as Analytics)
|
|
24
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
25
|
+
expect(window.heap.appid).toEqual(HEAP_TEST_ENV_ID)
|
|
26
|
+
})
|
|
27
|
+
test('loading with cdn', async () => {
|
|
28
|
+
jest.spyOn(destination, 'initialize')
|
|
29
|
+
|
|
30
|
+
mockHeapJsHttpRequest()
|
|
31
|
+
|
|
32
|
+
const [event] = await heapDestination({
|
|
33
|
+
appId: HEAP_TEST_ENV_ID,
|
|
34
|
+
subscriptions,
|
|
35
|
+
hostname: 'cdn.heapanalytics.com',
|
|
36
|
+
trackingServer: 'https://heapanalytics.com'
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
await event.load(Context.system(), {} as Analytics)
|
|
40
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
41
|
+
expect(window.heap.appid).toEqual(HEAP_TEST_ENV_ID)
|
|
42
|
+
})
|
|
43
|
+
})
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HEAP_SEGMENT_BROWSER_LIBRARY_NAME = 'browser-destination'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Settings {
|
|
4
|
+
/**
|
|
5
|
+
* The app ID of the environment to which you want to send data. You can find this ID on the [Projects](https://heapanalytics.com/app/manage/projects) page.
|
|
6
|
+
*/
|
|
7
|
+
appId: string
|
|
8
|
+
/**
|
|
9
|
+
* Setting to true will redact all target text on your website. For more information visit the heap [docs page](https://developers.heap.io/docs/web#global-data-redaction-via-disabling-text-capture).
|
|
10
|
+
*/
|
|
11
|
+
disableTextCapture?: boolean
|
|
12
|
+
/**
|
|
13
|
+
* This option is turned off by default to accommodate websites not served over HTTPS. If your application uses HTTPS, we recommend enabling secure cookies to prevent Heap cookies from being observed by unauthorized parties. For more information visit the heap [docs page](https://developers.heap.io/docs/web#securecookie).
|
|
14
|
+
*/
|
|
15
|
+
secureCookie?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* This is an optional setting. This is used to set up first-party data collection. For most cased this should not be set. For more information visit the heap [docs page](https://developers.heap.io/docs/set-up-first-party-data-collection-in-heap).
|
|
18
|
+
*/
|
|
19
|
+
trackingServer?: string
|
|
20
|
+
/**
|
|
21
|
+
* This is an optional setting used to set the host that loads heap-js. This setting is used when heapJS is self-hosted. In most cased this should be left unset. The hostname should not contain https or app id it will be populated like so: https://${hostname}/js/heap-${appId}.js. For more information visit the heap [docs page](https://developers.heap.io/docs/self-hosting-heapjs).
|
|
22
|
+
*/
|
|
23
|
+
hostname?: string
|
|
24
|
+
/**
|
|
25
|
+
* This is an optional setting. When set, nested array items will be sent in as new Heap events. Defaults to 0.
|
|
26
|
+
*/
|
|
27
|
+
browserArrayLimit?: number
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import {
|
|
3
|
+
createMockedHeapJsSdk,
|
|
4
|
+
HEAP_TEST_ENV_ID,
|
|
5
|
+
identifyUserSubscription,
|
|
6
|
+
mockHeapJsHttpRequest
|
|
7
|
+
} from '../../test-utilities'
|
|
8
|
+
import heapDestination from '../../index'
|
|
9
|
+
|
|
10
|
+
describe('#identify', () => {
|
|
11
|
+
it('should not call identify if user id is not provided and anonymous user id is provided', async () => {
|
|
12
|
+
mockHeapJsHttpRequest()
|
|
13
|
+
window.heap = createMockedHeapJsSdk()
|
|
14
|
+
|
|
15
|
+
const [identifyUser] = await heapDestination({ appId: HEAP_TEST_ENV_ID, subscriptions: [identifyUserSubscription] })
|
|
16
|
+
|
|
17
|
+
await identifyUser.load(Context.system(), {} as Analytics)
|
|
18
|
+
const heapIdentifySpy = jest.spyOn(window.heap, 'identify')
|
|
19
|
+
|
|
20
|
+
await identifyUser.identify?.(
|
|
21
|
+
new Context({
|
|
22
|
+
type: 'identify',
|
|
23
|
+
anonymousId: 'anon',
|
|
24
|
+
traits: {
|
|
25
|
+
testProp: false
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
expect(heapIdentifySpy).not.toHaveBeenCalled()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should call identify if user id is provided', async () => {
|
|
34
|
+
mockHeapJsHttpRequest()
|
|
35
|
+
window.heap = createMockedHeapJsSdk()
|
|
36
|
+
|
|
37
|
+
const [identifyUser] = await heapDestination({ appId: HEAP_TEST_ENV_ID, subscriptions: [identifyUserSubscription] })
|
|
38
|
+
|
|
39
|
+
await identifyUser.load(Context.system(), {} as Analytics)
|
|
40
|
+
const heapIdentifySpy = jest.spyOn(window.heap, 'identify')
|
|
41
|
+
const heapAddUserPropertiesSpy = jest.spyOn(window.heap, 'addUserProperties')
|
|
42
|
+
|
|
43
|
+
await identifyUser.identify?.(
|
|
44
|
+
new Context({
|
|
45
|
+
type: 'identify',
|
|
46
|
+
anonymousId: 'anon',
|
|
47
|
+
userId: 'user@example.com'
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(heapIdentifySpy).toHaveBeenCalledWith('user@example.com')
|
|
52
|
+
expect(heapAddUserPropertiesSpy).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should call addUserProprties if traits are provided', async () => {
|
|
56
|
+
mockHeapJsHttpRequest()
|
|
57
|
+
window.heap = createMockedHeapJsSdk()
|
|
58
|
+
|
|
59
|
+
const [identifyUser] = await heapDestination({ appId: HEAP_TEST_ENV_ID, subscriptions: [identifyUserSubscription] })
|
|
60
|
+
|
|
61
|
+
await identifyUser.load(Context.system(), {} as Analytics)
|
|
62
|
+
const heapIdentifySpy = jest.spyOn(window.heap, 'identify')
|
|
63
|
+
const heapAddUserPropertiesSpy = jest.spyOn(window.heap, 'addUserProperties')
|
|
64
|
+
|
|
65
|
+
await identifyUser.identify?.(
|
|
66
|
+
new Context({
|
|
67
|
+
type: 'identify',
|
|
68
|
+
anonymousId: 'anon',
|
|
69
|
+
traits: {
|
|
70
|
+
testProp: false
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
expect(heapIdentifySpy).not.toHaveBeenCalled()
|
|
76
|
+
expect(heapAddUserPropertiesSpy).toHaveBeenCalledWith({ testProp: false })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
|
|
2
|
+
import type { Settings } from '../generated-types'
|
|
3
|
+
import type { Payload } from './generated-types'
|
|
4
|
+
import { HeapApi } from '../types'
|
|
5
|
+
|
|
6
|
+
const action: BrowserActionDefinition<Settings, HeapApi, Payload> = {
|
|
7
|
+
title: 'Identify User',
|
|
8
|
+
description: 'Sets user identity',
|
|
9
|
+
platform: 'web',
|
|
10
|
+
defaultSubscription: 'type = "identify"',
|
|
11
|
+
fields: {
|
|
12
|
+
userId: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: false,
|
|
15
|
+
description: "The user's identity",
|
|
16
|
+
label: 'Identity',
|
|
17
|
+
default: {
|
|
18
|
+
'@path': '$.userId'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
traits: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
required: false,
|
|
24
|
+
description: 'The Segment traits to be forwarded to Heap',
|
|
25
|
+
label: 'Traits',
|
|
26
|
+
default: {
|
|
27
|
+
'@path': '$.traits'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
perform: (heap, event) => {
|
|
32
|
+
if (event.payload.traits) {
|
|
33
|
+
heap.addUserProperties(event.payload.traits)
|
|
34
|
+
}
|
|
35
|
+
if (event.payload.userId) {
|
|
36
|
+
heap.identify(event.payload.userId)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default action
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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 { HeapApi, UserConfig } from './types'
|
|
5
|
+
import { defaultValues } from '@segment/actions-core'
|
|
6
|
+
import trackEvent from './trackEvent'
|
|
7
|
+
import identifyUser from './identifyUser'
|
|
8
|
+
import { isDefined } from './utils'
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
heap: HeapApi
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Switch from unknown to the partner SDK client types
|
|
17
|
+
export const destination: BrowserDestinationDefinition<Settings, HeapApi> = {
|
|
18
|
+
name: 'Heap Web (Actions)',
|
|
19
|
+
slug: 'actions-heap-web',
|
|
20
|
+
mode: 'device',
|
|
21
|
+
presets: [
|
|
22
|
+
{
|
|
23
|
+
name: 'Track Event',
|
|
24
|
+
subscribe: 'type = "track"',
|
|
25
|
+
partnerAction: 'trackEvent',
|
|
26
|
+
mapping: defaultValues(trackEvent.fields)
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Identify User',
|
|
30
|
+
subscribe: 'type = "identify"',
|
|
31
|
+
partnerAction: 'identifyUser',
|
|
32
|
+
mapping: defaultValues(identifyUser.fields)
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
settings: {
|
|
36
|
+
// Add any Segment destination settings required here
|
|
37
|
+
appId: {
|
|
38
|
+
label: 'Heap app ID',
|
|
39
|
+
description:
|
|
40
|
+
'The app ID of the environment to which you want to send data. You can find this ID on the [Projects](https://heapanalytics.com/app/manage/projects) page.',
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
disableTextCapture: {
|
|
45
|
+
label: 'Global data redaction via Disabling Text Capture',
|
|
46
|
+
description:
|
|
47
|
+
'Setting to true will redact all target text on your website. For more information visit the heap [docs page](https://developers.heap.io/docs/web#global-data-redaction-via-disabling-text-capture).',
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
required: false
|
|
50
|
+
},
|
|
51
|
+
secureCookie: {
|
|
52
|
+
label: 'Secure Cookie',
|
|
53
|
+
description:
|
|
54
|
+
'This option is turned off by default to accommodate websites not served over HTTPS. If your application uses HTTPS, we recommend enabling secure cookies to prevent Heap cookies from being observed by unauthorized parties. For more information visit the heap [docs page](https://developers.heap.io/docs/web#securecookie).',
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
required: false
|
|
57
|
+
},
|
|
58
|
+
trackingServer: {
|
|
59
|
+
label: 'Tracking Server',
|
|
60
|
+
description:
|
|
61
|
+
'This is an optional setting. This is used to set up first-party data collection. For most cased this should not be set. For more information visit the heap [docs page](https://developers.heap.io/docs/set-up-first-party-data-collection-in-heap).',
|
|
62
|
+
type: 'string',
|
|
63
|
+
required: false
|
|
64
|
+
},
|
|
65
|
+
hostname: {
|
|
66
|
+
label: 'Hostname',
|
|
67
|
+
description:
|
|
68
|
+
'This is an optional setting used to set the host that loads heap-js. This setting is used when heapJS is self-hosted. In most cased this should be left unset. The hostname should not contain https or app id it will be populated like so: https://${hostname}/js/heap-${appId}.js. For more information visit the heap [docs page](https://developers.heap.io/docs/self-hosting-heapjs).',
|
|
69
|
+
type: 'string',
|
|
70
|
+
required: false
|
|
71
|
+
},
|
|
72
|
+
browserArrayLimit: {
|
|
73
|
+
label: 'Browser Array Limit',
|
|
74
|
+
description:
|
|
75
|
+
'This is an optional setting. When set, nested array items will be sent in as new Heap events. Defaults to 0.',
|
|
76
|
+
type: 'number',
|
|
77
|
+
required: false
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
initialize: async ({ settings }, deps) => {
|
|
82
|
+
if (window.heap) {
|
|
83
|
+
return window.heap
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const config: UserConfig = {
|
|
87
|
+
disableTextCapture: settings.disableTextCapture || false,
|
|
88
|
+
secureCookie: settings.secureCookie || false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (settings.trackingServer) {
|
|
92
|
+
config.trackingServer = settings.trackingServer
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// heap.appid and heap.config must be set before loading heap.js.
|
|
96
|
+
window.heap = window.heap || []
|
|
97
|
+
window.heap.appid = settings.appId
|
|
98
|
+
window.heap.config = config
|
|
99
|
+
|
|
100
|
+
if (isDefined(settings.hostname)) {
|
|
101
|
+
await deps.loadScript(`https://${settings.hostname}/js/heap-${settings.appId}.js`)
|
|
102
|
+
} else {
|
|
103
|
+
await deps.loadScript(`https://cdn.heapanalytics.com/js/heap-${settings.appId}.js`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Explained here: https://stackoverflow.com/questions/14859058/why-does-the-segment-io-loader-script-push-method-names-args-onto-a-queue-which
|
|
107
|
+
await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'heap'), 100)
|
|
108
|
+
|
|
109
|
+
return window.heap
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
actions: {
|
|
113
|
+
trackEvent,
|
|
114
|
+
identifyUser
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default browserDestination(destination)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { HeapApi } from './types'
|
|
2
|
+
import { Subscription } from '@segment/browser-destination-runtime/types'
|
|
3
|
+
import nock from 'nock'
|
|
4
|
+
|
|
5
|
+
export const HEAP_TEST_ENV_ID = '1'
|
|
6
|
+
|
|
7
|
+
export const createMockedHeapJsSdk = (): HeapApi => {
|
|
8
|
+
return {
|
|
9
|
+
appid: HEAP_TEST_ENV_ID,
|
|
10
|
+
config: {
|
|
11
|
+
disableTextCapture: true,
|
|
12
|
+
secureCookie: true
|
|
13
|
+
},
|
|
14
|
+
load: jest.fn(),
|
|
15
|
+
track: jest.fn(),
|
|
16
|
+
identify: jest.fn(),
|
|
17
|
+
addUserProperties: jest.fn()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const trackEventSubscription: Subscription = {
|
|
21
|
+
partnerAction: 'trackEvent',
|
|
22
|
+
name: 'Track Event',
|
|
23
|
+
enabled: true,
|
|
24
|
+
subscribe: 'type = "track"',
|
|
25
|
+
mapping: {
|
|
26
|
+
name: {
|
|
27
|
+
'@path': '$.name'
|
|
28
|
+
},
|
|
29
|
+
properties: {
|
|
30
|
+
'@path': '$.properties'
|
|
31
|
+
},
|
|
32
|
+
identity: {
|
|
33
|
+
'@path': '$.userId'
|
|
34
|
+
},
|
|
35
|
+
anonymousId: {
|
|
36
|
+
'@path': '$.anonymousId'
|
|
37
|
+
},
|
|
38
|
+
traits: {
|
|
39
|
+
'@path': '$.context.traits'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const identifyUserSubscription: Subscription = {
|
|
45
|
+
partnerAction: 'identifyUser',
|
|
46
|
+
name: 'Identify User',
|
|
47
|
+
enabled: true,
|
|
48
|
+
subscribe: 'type = "identify"',
|
|
49
|
+
mapping: {
|
|
50
|
+
anonymousId: {
|
|
51
|
+
'@path': '$.anonymousId'
|
|
52
|
+
},
|
|
53
|
+
userId: {
|
|
54
|
+
'@path': '$.userId'
|
|
55
|
+
},
|
|
56
|
+
traits: {
|
|
57
|
+
'@path': '$.traits'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const mockHeapJsHttpRequest = (): void => {
|
|
63
|
+
nock('https://cdn.heapanalytics.com').get(`/js/heap-${HEAP_TEST_ENV_ID}.js`).reply(200, {})
|
|
64
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Analytics, Context, Plugin } from '@segment/analytics-next'
|
|
2
|
+
import heapDestination from '../../index'
|
|
3
|
+
import {
|
|
4
|
+
createMockedHeapJsSdk,
|
|
5
|
+
HEAP_TEST_ENV_ID,
|
|
6
|
+
mockHeapJsHttpRequest,
|
|
7
|
+
trackEventSubscription
|
|
8
|
+
} from '../../test-utilities'
|
|
9
|
+
import { HEAP_SEGMENT_BROWSER_LIBRARY_NAME } from '../../constants'
|
|
10
|
+
|
|
11
|
+
describe('#trackEvent', () => {
|
|
12
|
+
let event: Plugin
|
|
13
|
+
let heapTrackSpy: jest.SpyInstance
|
|
14
|
+
let addUserPropertiesSpy: jest.SpyInstance
|
|
15
|
+
let identifySpy: jest.SpyInstance
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
mockHeapJsHttpRequest()
|
|
19
|
+
window.heap = createMockedHeapJsSdk()
|
|
20
|
+
|
|
21
|
+
event = (
|
|
22
|
+
await heapDestination({ appId: HEAP_TEST_ENV_ID, subscriptions: [trackEventSubscription], browserArrayLimit: 5 })
|
|
23
|
+
)[0]
|
|
24
|
+
|
|
25
|
+
await event.load(Context.system(), {} as Analytics)
|
|
26
|
+
heapTrackSpy = jest.spyOn(window.heap, 'track')
|
|
27
|
+
addUserPropertiesSpy = jest.spyOn(window.heap, 'addUserProperties')
|
|
28
|
+
identifySpy = jest.spyOn(window.heap, 'identify')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.resetAllMocks()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('sends events to heap', async () => {
|
|
36
|
+
await event.track?.(
|
|
37
|
+
new Context({
|
|
38
|
+
type: 'track',
|
|
39
|
+
name: 'hello!',
|
|
40
|
+
properties: {
|
|
41
|
+
banana: '📞',
|
|
42
|
+
apple: [
|
|
43
|
+
{
|
|
44
|
+
carrot: 12,
|
|
45
|
+
broccoli: [
|
|
46
|
+
{
|
|
47
|
+
onion: 'crisp',
|
|
48
|
+
tomato: 'fruit'
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
carrot: 21,
|
|
54
|
+
broccoli: [
|
|
55
|
+
{
|
|
56
|
+
tomato: 'vegetable'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tomato: 'fruit'
|
|
60
|
+
},
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
pickle: 'vinegar'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
pie: 3.1415
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
emptyArray: [],
|
|
73
|
+
float: 1.2345,
|
|
74
|
+
booleanTrue: true,
|
|
75
|
+
booleanFalse: false,
|
|
76
|
+
nullValue: null,
|
|
77
|
+
undefinedValue: undefined
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
expect(heapTrackSpy).toHaveBeenCalledTimes(3)
|
|
82
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(1, 'hello! apple item', {
|
|
83
|
+
carrot: 12,
|
|
84
|
+
'broccoli.0.onion': 'crisp',
|
|
85
|
+
'broccoli.0.tomato': 'fruit',
|
|
86
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
87
|
+
})
|
|
88
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(2, 'hello! apple item', {
|
|
89
|
+
carrot: 21,
|
|
90
|
+
'broccoli.0.tomato': 'vegetable',
|
|
91
|
+
'broccoli.1.tomato': 'fruit',
|
|
92
|
+
'broccoli.2.0.pickle': 'vinegar',
|
|
93
|
+
'broccoli.2.1.pie': '3.1415',
|
|
94
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
95
|
+
})
|
|
96
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(3, 'hello!', {
|
|
97
|
+
banana: '📞',
|
|
98
|
+
float: 1.2345,
|
|
99
|
+
booleanTrue: true,
|
|
100
|
+
booleanFalse: false,
|
|
101
|
+
nullValue: null,
|
|
102
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME,
|
|
103
|
+
'apple.0.broccoli.0.onion': 'crisp',
|
|
104
|
+
'apple.0.broccoli.0.tomato': 'fruit',
|
|
105
|
+
'apple.0.carrot': '12',
|
|
106
|
+
'apple.1.broccoli.0.tomato': 'vegetable',
|
|
107
|
+
'apple.1.broccoli.1.tomato': 'fruit',
|
|
108
|
+
'apple.1.broccoli.2.0.pickle': 'vinegar',
|
|
109
|
+
'apple.1.broccoli.2.1.pie': '3.1415',
|
|
110
|
+
'apple.1.carrot': '21'
|
|
111
|
+
})
|
|
112
|
+
expect(addUserPropertiesSpy).toHaveBeenCalledTimes(0)
|
|
113
|
+
expect(identifySpy).toHaveBeenCalledTimes(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('limits number of properties in array', async () => {
|
|
117
|
+
await event.track?.(
|
|
118
|
+
new Context({
|
|
119
|
+
type: 'track',
|
|
120
|
+
name: 'hello!',
|
|
121
|
+
properties: {
|
|
122
|
+
testArray1: [{ val: 1 }, { val: 2 }, { val: 3 }],
|
|
123
|
+
testArray2: [{ val: 4 }, { val: 5 }, { val: 'N/A' }]
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
expect(heapTrackSpy).toHaveBeenCalledTimes(6)
|
|
128
|
+
|
|
129
|
+
for (let i = 1; i <= 3; i++) {
|
|
130
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(i, 'hello! testArray1 item', {
|
|
131
|
+
val: i,
|
|
132
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
for (let i = 4; i <= 5; i++) {
|
|
136
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(i, 'hello! testArray2 item', {
|
|
137
|
+
val: i,
|
|
138
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
expect(heapTrackSpy).toHaveBeenNthCalledWith(6, 'hello!', {
|
|
142
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME,
|
|
143
|
+
'testArray1.0.val': '1',
|
|
144
|
+
'testArray1.1.val': '2',
|
|
145
|
+
'testArray1.2.val': '3',
|
|
146
|
+
'testArray2.0.val': '4',
|
|
147
|
+
'testArray2.1.val': '5',
|
|
148
|
+
'testArray2.2.val': 'N/A'
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should send segment_library property if no other properties were provided', async () => {
|
|
153
|
+
await event.track?.(
|
|
154
|
+
new Context({
|
|
155
|
+
type: 'track',
|
|
156
|
+
name: 'hello!'
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
|
|
161
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
162
|
+
})
|
|
163
|
+
expect(addUserPropertiesSpy).toHaveBeenCalledTimes(0)
|
|
164
|
+
expect(identifySpy).toHaveBeenCalledTimes(0)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should not override segment_library property value if provided by user', async () => {
|
|
168
|
+
const segmentLibraryValue = 'user-provided-value'
|
|
169
|
+
const userId = 'TEST_ID77'
|
|
170
|
+
await event.track?.(
|
|
171
|
+
new Context({
|
|
172
|
+
type: 'track',
|
|
173
|
+
name: 'hello!',
|
|
174
|
+
properties: {
|
|
175
|
+
segment_library: segmentLibraryValue
|
|
176
|
+
},
|
|
177
|
+
userId
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
|
|
182
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
183
|
+
})
|
|
184
|
+
expect(identifySpy).toHaveBeenCalledWith(userId)
|
|
185
|
+
expect(addUserPropertiesSpy).toHaveBeenCalledTimes(0)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should add traits', async () => {
|
|
189
|
+
const segmentLibraryValue = 'test123'
|
|
190
|
+
const anonymous_id = 'ANON1'
|
|
191
|
+
const name = 'Grace Hopper'
|
|
192
|
+
await event.track?.(
|
|
193
|
+
new Context({
|
|
194
|
+
type: 'track',
|
|
195
|
+
name: 'hello!',
|
|
196
|
+
properties: {
|
|
197
|
+
segment_library: segmentLibraryValue
|
|
198
|
+
},
|
|
199
|
+
context: {
|
|
200
|
+
traits: {
|
|
201
|
+
name
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
anonymousId: anonymous_id
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
|
|
209
|
+
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
210
|
+
})
|
|
211
|
+
expect(addUserPropertiesSpy).toHaveBeenCalledWith({
|
|
212
|
+
anonymous_id,
|
|
213
|
+
name
|
|
214
|
+
})
|
|
215
|
+
expect(identifySpy).toHaveBeenCalledTimes(0)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Payload {
|
|
4
|
+
/**
|
|
5
|
+
* The name of the event.
|
|
6
|
+
*/
|
|
7
|
+
name: string
|
|
8
|
+
/**
|
|
9
|
+
* A JSON object containing additional information about the event that will be indexed by Heap.
|
|
10
|
+
*/
|
|
11
|
+
properties?: {
|
|
12
|
+
[k: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* a string that uniquely identifies a user, such as an email, handle, or username. This means no two users in one environment may share the same identity. More on identify: https://developers.heap.io/docs/using-identify
|
|
16
|
+
*/
|
|
17
|
+
identity?: string
|
|
18
|
+
/**
|
|
19
|
+
* The segment anonymous identifier for the user
|
|
20
|
+
*/
|
|
21
|
+
anonymousId?: string
|
|
22
|
+
/**
|
|
23
|
+
* An object with key-value properties you want associated with the user. Each property must either be a number or string with fewer than 1024 characters.
|
|
24
|
+
*/
|
|
25
|
+
traits?: {
|
|
26
|
+
[k: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
|
|
2
|
+
import type { Settings } from '../generated-types'
|
|
3
|
+
import type { Payload } from './generated-types'
|
|
4
|
+
import { HeapApi } from '../types'
|
|
5
|
+
import { HEAP_SEGMENT_BROWSER_LIBRARY_NAME } from '../constants'
|
|
6
|
+
import { isDefined, flat, flattenProperties } from '../utils'
|
|
7
|
+
|
|
8
|
+
const action: BrowserActionDefinition<Settings, HeapApi, Payload> = {
|
|
9
|
+
title: 'Track Event',
|
|
10
|
+
description: 'Track events',
|
|
11
|
+
platform: 'web',
|
|
12
|
+
defaultSubscription: 'type = "track"',
|
|
13
|
+
fields: {
|
|
14
|
+
name: {
|
|
15
|
+
description: 'The name of the event.',
|
|
16
|
+
label: 'Name',
|
|
17
|
+
required: true,
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: {
|
|
20
|
+
'@path': '$.event'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
properties: {
|
|
24
|
+
description: 'A JSON object containing additional information about the event that will be indexed by Heap.',
|
|
25
|
+
label: 'Properties',
|
|
26
|
+
required: false,
|
|
27
|
+
type: 'object',
|
|
28
|
+
default: {
|
|
29
|
+
'@path': '$.properties'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
identity: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
required: false,
|
|
35
|
+
label: 'Identity',
|
|
36
|
+
description:
|
|
37
|
+
'a string that uniquely identifies a user, such as an email, handle, or username. This means no two users in one environment may share the same identity. More on identify: https://developers.heap.io/docs/using-identify'
|
|
38
|
+
},
|
|
39
|
+
anonymousId: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
required: false,
|
|
42
|
+
description: 'The segment anonymous identifier for the user',
|
|
43
|
+
label: 'Anonymous ID',
|
|
44
|
+
default: {
|
|
45
|
+
'@path': '$.anonymousId'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
traits: {
|
|
49
|
+
label: 'User Properties',
|
|
50
|
+
type: 'object',
|
|
51
|
+
description:
|
|
52
|
+
'An object with key-value properties you want associated with the user. Each property must either be a number or string with fewer than 1024 characters.',
|
|
53
|
+
default: {
|
|
54
|
+
'@path': '$.context.traits'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
perform: (heap, event) => {
|
|
59
|
+
// Add user properties
|
|
60
|
+
if (event.payload.anonymousId || isDefined(event.payload?.traits)) {
|
|
61
|
+
const traits = flat(event.payload?.traits)
|
|
62
|
+
heap.addUserProperties({
|
|
63
|
+
...(isDefined(event.payload.anonymousId) && { anonymous_id: event.payload.anonymousId }),
|
|
64
|
+
...(isDefined(traits) && traits)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Identify user
|
|
69
|
+
if (event.payload?.identity && isDefined(event.payload?.identity)) {
|
|
70
|
+
heap.identify(event.payload.identity)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track Events
|
|
74
|
+
let eventProperties = Object.assign({}, event.payload.properties)
|
|
75
|
+
const eventName = event.payload.name
|
|
76
|
+
const browserArrayLimit = event.settings.browserArrayLimit || 0
|
|
77
|
+
const browserArrayLimitSet = !!browserArrayLimit
|
|
78
|
+
let arrayEventsCount = 0
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of Object.entries(eventProperties)) {
|
|
81
|
+
if (browserArrayLimitSet && arrayEventsCount >= browserArrayLimit) {
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(value)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
delete eventProperties[key]
|
|
90
|
+
eventProperties = { ...eventProperties, ...flat({ [key]: value }) }
|
|
91
|
+
|
|
92
|
+
const arrayLength = value.length
|
|
93
|
+
let arrayPropertyValues
|
|
94
|
+
// truncate in case there are multiple array properties
|
|
95
|
+
if (browserArrayLimitSet && arrayLength + arrayEventsCount > browserArrayLimit) {
|
|
96
|
+
arrayPropertyValues = value.splice(0, browserArrayLimit - arrayEventsCount)
|
|
97
|
+
} else {
|
|
98
|
+
arrayPropertyValues = value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
arrayEventsCount += arrayLength
|
|
102
|
+
|
|
103
|
+
arrayPropertyValues.forEach((arrayPropertyValue) => {
|
|
104
|
+
const arrayProperties = flattenProperties(arrayPropertyValue)
|
|
105
|
+
heapTrack(heap, `${eventName} ${key} item`, arrayProperties)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
heapTrack(heap, eventName, eventProperties)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const heapTrack = (
|
|
113
|
+
heap: HeapApi,
|
|
114
|
+
eventName: string,
|
|
115
|
+
properties: {
|
|
116
|
+
[k: string]: unknown
|
|
117
|
+
}
|
|
118
|
+
) => {
|
|
119
|
+
properties.segment_library = HEAP_SEGMENT_BROWSER_LIBRARY_NAME
|
|
120
|
+
heap.track(eventName, properties)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default action
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type UserConfig = {
|
|
2
|
+
trackingServer?: string
|
|
3
|
+
disableTextCapture: boolean
|
|
4
|
+
secureCookie: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type UserProperties = {
|
|
8
|
+
[k: string]: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type EventProperties = {
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type HeapApi = {
|
|
16
|
+
appid: string
|
|
17
|
+
track: (eventName: string, eventProperties: EventProperties, library?: string) => void
|
|
18
|
+
load: () => void
|
|
19
|
+
config: UserConfig
|
|
20
|
+
identify: (identity: string) => void
|
|
21
|
+
addUserProperties: (properties: UserProperties) => void
|
|
22
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function isDefined(value: string | undefined | null | number | object): boolean {
|
|
2
|
+
if (typeof value === 'object') {
|
|
3
|
+
return !!value && Object.keys(value).length !== 0
|
|
4
|
+
}
|
|
5
|
+
return !(value === undefined || value === null || value === '' || value === 0 || value === '0')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Properties = {
|
|
9
|
+
[k: string]: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FlattenProperties = object & {
|
|
13
|
+
[k: string]: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function flat(data?: Properties, prefix = ''): FlattenProperties | undefined {
|
|
17
|
+
if (!isDefined(data)) {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
let result: FlattenProperties = {}
|
|
21
|
+
for (const key in data) {
|
|
22
|
+
if (typeof data[key] == 'object' && data[key] !== null) {
|
|
23
|
+
const flatten = flat(data[key] as Properties, prefix + '.' + key)
|
|
24
|
+
result = { ...result, ...flatten }
|
|
25
|
+
} else {
|
|
26
|
+
const stringifiedValue = stringify(data[key])
|
|
27
|
+
result[(prefix + '.' + key).replace(/^\./, '')] = stringifiedValue
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
export const flattenProperties = (arrayPropertyValue: any) => {
|
|
35
|
+
let arrayProperties = {}
|
|
36
|
+
for (const [key, value] of Object.entries(arrayPropertyValue)) {
|
|
37
|
+
if (typeof value == 'object' && value !== null) {
|
|
38
|
+
arrayProperties = { ...arrayProperties, ...flat({ [key]: value as Properties }) }
|
|
39
|
+
} else {
|
|
40
|
+
arrayProperties = Object.assign(arrayProperties, { [key]: value })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return arrayProperties
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringify(value: unknown): string {
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
51
|
+
return value.toString()
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(value)
|
|
54
|
+
}
|