@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 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
+ })
@@ -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,14 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The user's identity
6
+ */
7
+ userId?: string
8
+ /**
9
+ * The Segment traits to be forwarded to Heap
10
+ */
11
+ traits?: {
12
+ [k: string]: unknown
13
+ }
14
+ }
@@ -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
+ }
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
+ }