@segment/analytics-browser-actions-hubspot 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 +70 -0
- package/src/generated-types.ts +24 -0
- package/src/index.ts +109 -0
- package/src/trackCustomBehavioralEvent/__tests__/index.test.ts +124 -0
- package/src/trackCustomBehavioralEvent/generated-types.ts +14 -0
- package/src/trackCustomBehavioralEvent/index.ts +48 -0
- package/src/trackPageView/__tests__/index.test.ts +72 -0
- package/src/trackPageView/generated-types.ts +8 -0
- package/src/trackPageView/index.ts +28 -0
- package/src/types.ts +3 -0
- package/src/upsertContact/__tests__/index.test.ts +207 -0
- package/src/upsertContact/generated-types.ts +42 -0
- package/src/upsertContact/index.ts +121 -0
- package/src/utils/flatten.ts +30 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@segment/analytics-browser-actions-hubspot",
|
|
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,70 @@
|
|
|
1
|
+
import { Subscription } from '@segment/browser-destination-runtime/types'
|
|
2
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
3
|
+
import hubspotDestination, { destination } from '../index'
|
|
4
|
+
import nock from 'nock'
|
|
5
|
+
|
|
6
|
+
const subscriptions: Subscription[] = [
|
|
7
|
+
{
|
|
8
|
+
partnerAction: 'trackCustomBehavioralEvent',
|
|
9
|
+
name: 'Track Custom Behavioral Event',
|
|
10
|
+
enabled: true,
|
|
11
|
+
subscribe: 'type = "track"',
|
|
12
|
+
mapping: {
|
|
13
|
+
event_name: {
|
|
14
|
+
'@path': '$.event'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
describe('Hubspot Web (Actions)', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
nock('https://js.hs-scripts.com/').get('/12345.js').reply(200, "window._hsq = '๐บ๐ธ'")
|
|
23
|
+
nock('https://js-eu1.hs-scripts.com/').get('/12345.js').reply(200, "window._hsq = '๐ช๐บ'")
|
|
24
|
+
nock('https://https://js.hsforms.net').get('forms/v2.js').reply(200, "window.hbspt = {forms: '1232'}")
|
|
25
|
+
})
|
|
26
|
+
test('loads hubspot analytics with just a HubID', async () => {
|
|
27
|
+
const [event] = await hubspotDestination({
|
|
28
|
+
portalId: '12345',
|
|
29
|
+
subscriptions
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
jest.spyOn(destination, 'initialize')
|
|
33
|
+
|
|
34
|
+
await event.load(Context.system(), {} as Analytics)
|
|
35
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
36
|
+
expect(window._hsq).toEqual('๐บ๐ธ')
|
|
37
|
+
expect(window.hbspt).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('loads hubspot analytics with EU script', async () => {
|
|
41
|
+
const [event] = await hubspotDestination({
|
|
42
|
+
portalId: '12345',
|
|
43
|
+
enableEuropeanDataCenter: true,
|
|
44
|
+
subscriptions
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
jest.spyOn(destination, 'initialize')
|
|
48
|
+
|
|
49
|
+
await event.load(Context.system(), {} as Analytics)
|
|
50
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
51
|
+
|
|
52
|
+
expect(window._hsq).toEqual('๐ช๐บ')
|
|
53
|
+
expect(window.hbspt).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('loads hubspot forms SDK', async () => {
|
|
57
|
+
const [event] = await hubspotDestination({
|
|
58
|
+
portalId: '12345',
|
|
59
|
+
loadFormsSDK: true,
|
|
60
|
+
subscriptions
|
|
61
|
+
})
|
|
62
|
+
jest.spyOn(destination, 'initialize')
|
|
63
|
+
|
|
64
|
+
await event.load(Context.system(), {} as Analytics)
|
|
65
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
66
|
+
|
|
67
|
+
expect(window._hsq).toEqual('๐บ๐ธ')
|
|
68
|
+
expect(window.hbspt).toBeDefined()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Settings {
|
|
4
|
+
/**
|
|
5
|
+
* The Hub ID of your HubSpot account.
|
|
6
|
+
*/
|
|
7
|
+
portalId: string
|
|
8
|
+
/**
|
|
9
|
+
* Enable this option if you would like Segment to load the HubSpot SDK for EU data residency.
|
|
10
|
+
*/
|
|
11
|
+
enableEuropeanDataCenter?: boolean
|
|
12
|
+
/**
|
|
13
|
+
* Enable this option to fire a `trackPageView` HubSpot event immediately after each Segment `identify` call to flush the data to HubSpot immediately.
|
|
14
|
+
*/
|
|
15
|
+
flushIdentifyImmediately?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Format the event names for custom behavioral event automatically to standard HubSpot format (`pe<HubID>_event_name`).
|
|
18
|
+
*/
|
|
19
|
+
formatCustomBehavioralEventNames?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Enable this option if you would like Segment to automatically load the HubSpot Forms SDK onto your site.
|
|
22
|
+
*/
|
|
23
|
+
loadFormsSDK?: boolean
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
5
|
+
import trackCustomBehavioralEvent from './trackCustomBehavioralEvent'
|
|
6
|
+
|
|
7
|
+
import trackPageView from './trackPageView'
|
|
8
|
+
|
|
9
|
+
import upsertContact from './upsertContact'
|
|
10
|
+
import type { Hubspot } from './types'
|
|
11
|
+
import { defaultValues } from '@segment/actions-core'
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
_hsq: any
|
|
16
|
+
hbspt: any
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Switch from unknown to the partner SDK client types
|
|
21
|
+
export const destination: BrowserDestinationDefinition<Settings, Hubspot> = {
|
|
22
|
+
name: 'Hubspot Web (Actions)',
|
|
23
|
+
slug: 'actions-hubspot-web',
|
|
24
|
+
mode: 'device',
|
|
25
|
+
presets: [
|
|
26
|
+
{
|
|
27
|
+
name: 'Track Custom Behavioral Event',
|
|
28
|
+
subscribe: 'type = "track"',
|
|
29
|
+
partnerAction: 'trackCustomBehavioralEvent',
|
|
30
|
+
mapping: defaultValues(trackCustomBehavioralEvent.fields)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Upsert Contact',
|
|
34
|
+
subscribe: 'type = "identify"',
|
|
35
|
+
partnerAction: 'upsertContact',
|
|
36
|
+
mapping: defaultValues(upsertContact.fields)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Track Page View',
|
|
40
|
+
subscribe: 'type = "page"',
|
|
41
|
+
partnerAction: 'trackPageView',
|
|
42
|
+
mapping: defaultValues(trackPageView.fields)
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
settings: {
|
|
46
|
+
portalId: {
|
|
47
|
+
description: 'The Hub ID of your HubSpot account.',
|
|
48
|
+
label: 'Hub ID',
|
|
49
|
+
type: 'string',
|
|
50
|
+
required: true
|
|
51
|
+
},
|
|
52
|
+
enableEuropeanDataCenter: {
|
|
53
|
+
description: 'Enable this option if you would like Segment to load the HubSpot SDK for EU data residency.',
|
|
54
|
+
label: 'Enable the European Data Center SDK.',
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
required: false
|
|
57
|
+
},
|
|
58
|
+
flushIdentifyImmediately: {
|
|
59
|
+
description: 'Enable this option to fire a `trackPageView` HubSpot event immediately after each Segment `identify` call to flush the data to HubSpot immediately.',
|
|
60
|
+
label: 'Flush Identify Calls Immediately',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
required: false
|
|
63
|
+
},
|
|
64
|
+
formatCustomBehavioralEventNames: {
|
|
65
|
+
description: 'Format the event names for custom behavioral event automatically to standard HubSpot format (`pe<HubID>_event_name`).',
|
|
66
|
+
label: 'Format Custom Behavioral Event Names',
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
required: false,
|
|
69
|
+
default: true
|
|
70
|
+
},
|
|
71
|
+
loadFormsSDK: {
|
|
72
|
+
description: 'Enable this option if you would like Segment to automatically load the HubSpot Forms SDK onto your site.',
|
|
73
|
+
label: 'Load Forms SDK',
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
required: false,
|
|
76
|
+
default: false
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
initialize: async ({ settings }, deps) => {
|
|
81
|
+
const scriptPath = settings.enableEuropeanDataCenter
|
|
82
|
+
? `https://js-eu1.hs-scripts.com/${settings.portalId}.js`
|
|
83
|
+
: `https://js.hs-scripts.com/${settings.portalId}.js`
|
|
84
|
+
|
|
85
|
+
const formsScriptPath = settings.enableEuropeanDataCenter
|
|
86
|
+
? 'https://js-eu1.hsforms.net/forms/v2.js'
|
|
87
|
+
: 'https://js.hsforms.net/forms/v2.js'
|
|
88
|
+
|
|
89
|
+
await deps.loadScript(scriptPath)
|
|
90
|
+
if (settings.loadFormsSDK) {
|
|
91
|
+
await deps.loadScript(formsScriptPath)
|
|
92
|
+
}
|
|
93
|
+
await deps.resolveWhen(
|
|
94
|
+
() =>
|
|
95
|
+
!!(window._hsq && window._hsq.push !== Array.prototype.push) &&
|
|
96
|
+
(!settings.loadFormsSDK || !!(window.hbspt && window.hbspt.forms)),
|
|
97
|
+
100
|
|
98
|
+
)
|
|
99
|
+
return window._hsq
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
actions: {
|
|
103
|
+
trackCustomBehavioralEvent,
|
|
104
|
+
trackPageView,
|
|
105
|
+
upsertContact
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default browserDestination(destination)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import { Subscription } from '@segment/browser-destination-runtime'
|
|
3
|
+
import hubspotDestination, { destination } from '../../index'
|
|
4
|
+
import { Hubspot } from '../../types'
|
|
5
|
+
|
|
6
|
+
const subscriptions: Subscription[] = [
|
|
7
|
+
{
|
|
8
|
+
partnerAction: 'trackCustomBehavioralEvent',
|
|
9
|
+
name: 'Track Custom Behavioral Event',
|
|
10
|
+
enabled: true,
|
|
11
|
+
subscribe: 'type = "track"',
|
|
12
|
+
mapping: {
|
|
13
|
+
name: {
|
|
14
|
+
'@path': '$.event'
|
|
15
|
+
},
|
|
16
|
+
properties: {
|
|
17
|
+
'@path': '$.properties'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
describe('Hubspot.trackCustomBehavioralEvent', () => {
|
|
24
|
+
const settings = {
|
|
25
|
+
portalId: '1234',
|
|
26
|
+
formatCustomBehavioralEventNames: true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let mockHubspot: Hubspot
|
|
30
|
+
let trackCustomBehavioralEvent: any
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
jest.restoreAllMocks()
|
|
33
|
+
|
|
34
|
+
const [trackCustomBehavioralEventPlugin] = await hubspotDestination({
|
|
35
|
+
...settings,
|
|
36
|
+
subscriptions
|
|
37
|
+
})
|
|
38
|
+
trackCustomBehavioralEvent = trackCustomBehavioralEventPlugin
|
|
39
|
+
|
|
40
|
+
jest.spyOn(destination, 'initialize').mockImplementation(() => {
|
|
41
|
+
mockHubspot = {
|
|
42
|
+
push: jest.fn()
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve(mockHubspot)
|
|
45
|
+
})
|
|
46
|
+
await trackCustomBehavioralEvent.load(Context.system(), {} as Analytics)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('maps custom traits correctly', async () => {
|
|
50
|
+
const context = new Context({
|
|
51
|
+
type: 'track',
|
|
52
|
+
event: 'purchased a ๐ฑ',
|
|
53
|
+
properties: {
|
|
54
|
+
type: '๐ฃ',
|
|
55
|
+
price: '$12.00',
|
|
56
|
+
currency: 'USD'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
await trackCustomBehavioralEvent.track?.(context)
|
|
60
|
+
|
|
61
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
62
|
+
'trackCustomBehavioralEvent',
|
|
63
|
+
{ name: 'pe1234_purchased_a_๐ฑ', properties: { currency: 'USD', price: '$12.00', type: '๐ฃ' } }
|
|
64
|
+
])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('flattens nested object properties', async () => {
|
|
68
|
+
const context = new Context({
|
|
69
|
+
type: 'track',
|
|
70
|
+
event: 'purchased a ๐ฑ',
|
|
71
|
+
properties: {
|
|
72
|
+
type: '๐ฃ',
|
|
73
|
+
price: '$12.00',
|
|
74
|
+
currency: 'USD',
|
|
75
|
+
sides: {
|
|
76
|
+
item1: '๐ง',
|
|
77
|
+
item2: '๐ง',
|
|
78
|
+
'auxilery Sauces': {
|
|
79
|
+
'Soy Sauce': '๐ถ'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
await trackCustomBehavioralEvent.track?.(context)
|
|
85
|
+
|
|
86
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
87
|
+
'trackCustomBehavioralEvent',
|
|
88
|
+
{
|
|
89
|
+
name: 'pe1234_purchased_a_๐ฑ',
|
|
90
|
+
properties: {
|
|
91
|
+
currency: 'USD',
|
|
92
|
+
price: '$12.00',
|
|
93
|
+
type: '๐ฃ',
|
|
94
|
+
sides_item1: '๐ง',
|
|
95
|
+
sides_item2: '๐ง',
|
|
96
|
+
sides_auxilery_sauces_soy_sauce: '๐ถ'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('snake case spaces and dots', async () => {
|
|
103
|
+
const context = new Context({
|
|
104
|
+
type: 'track',
|
|
105
|
+
event: 'purchased a ๐ฑ',
|
|
106
|
+
properties: {
|
|
107
|
+
type: '๐ฃ',
|
|
108
|
+
price: '$12.00',
|
|
109
|
+
currency: 'USD',
|
|
110
|
+
'type of fish': '๐',
|
|
111
|
+
'brown.rice': false
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
await trackCustomBehavioralEvent.track?.(context)
|
|
115
|
+
|
|
116
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
117
|
+
'trackCustomBehavioralEvent',
|
|
118
|
+
{
|
|
119
|
+
name: 'pe1234_purchased_a_๐ฑ',
|
|
120
|
+
properties: { currency: 'USD', price: '$12.00', type: '๐ฃ', type_of_fish: '๐', brown_rice: false }
|
|
121
|
+
}
|
|
122
|
+
])
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Payload {
|
|
4
|
+
/**
|
|
5
|
+
* The internal event name assigned by HubSpot. This can be found in your HubSpot account. If the "Format Custom Behavioral Event Names" setting is enabled, Segment will automatically convert your Segment event name into the expected HubSpot internal event name format.
|
|
6
|
+
*/
|
|
7
|
+
name: string
|
|
8
|
+
/**
|
|
9
|
+
* A list of key-value pairs that describe the event.
|
|
10
|
+
*/
|
|
11
|
+
properties?: {
|
|
12
|
+
[k: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 type { Hubspot } from '../types'
|
|
5
|
+
import { flatten } from '../utils/flatten'
|
|
6
|
+
|
|
7
|
+
// Change from unknown to the partner SDK types
|
|
8
|
+
const action: BrowserActionDefinition<Settings, Hubspot, Payload> = {
|
|
9
|
+
title: 'Track Custom Behavioral Event',
|
|
10
|
+
description: 'Send a custom behavioral event to HubSpot.',
|
|
11
|
+
platform: 'web',
|
|
12
|
+
defaultSubscription: 'type = "track"',
|
|
13
|
+
fields: {
|
|
14
|
+
name: {
|
|
15
|
+
description: 'The internal event name assigned by HubSpot. This can be found in your HubSpot account. If the "Format Custom Behavioral Event Names" setting is enabled, Segment will automatically convert your Segment event name into the expected HubSpot internal event name format.',
|
|
16
|
+
label: 'Event Name',
|
|
17
|
+
type: 'string',
|
|
18
|
+
required: true,
|
|
19
|
+
default: {
|
|
20
|
+
'@path': '$.event'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
properties: {
|
|
24
|
+
description: 'A list of key-value pairs that describe the event.',
|
|
25
|
+
label: 'Event Properties',
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: false,
|
|
28
|
+
default: {
|
|
29
|
+
'@path': '$.properties'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
perform: (_hsq, event) => {
|
|
34
|
+
let { name, properties } = event.payload
|
|
35
|
+
|
|
36
|
+
if (event.settings.formatCustomBehavioralEventNames) {
|
|
37
|
+
name = `pe${event.settings.portalId}_${name.replace(/[\s.]+/g, '_').toLocaleLowerCase()}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// for custom properties, we will:
|
|
41
|
+
// remove any non-primitives, replace spaces and dots with underscores, then lowercase
|
|
42
|
+
properties = properties && flatten(properties, '', [], (key) => key.replace(/[\s.]+/g, '_').toLocaleLowerCase())
|
|
43
|
+
|
|
44
|
+
_hsq.push(['trackCustomBehavioralEvent', { name, properties }])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default action
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import { Subscription } from '@segment/browser-destination-runtime'
|
|
3
|
+
import hubspotDestination, { destination } from '../../index'
|
|
4
|
+
import { Hubspot } from '../../types'
|
|
5
|
+
|
|
6
|
+
const subscriptions: Subscription[] = [
|
|
7
|
+
{
|
|
8
|
+
partnerAction: 'trackPageView',
|
|
9
|
+
name: 'Track Page View',
|
|
10
|
+
enabled: true,
|
|
11
|
+
subscribe: 'type = "page"',
|
|
12
|
+
mapping: {
|
|
13
|
+
path: {
|
|
14
|
+
'@path': '$.context.page.path'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
describe('Hubspot.trackPageView', () => {
|
|
21
|
+
const settings = {
|
|
22
|
+
portalId: '1234'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let mockHubspot: Hubspot
|
|
26
|
+
let trackPageViewEvent: any
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
jest.restoreAllMocks()
|
|
29
|
+
|
|
30
|
+
const [trackPageViewEventPlugin] = await hubspotDestination({
|
|
31
|
+
...settings,
|
|
32
|
+
subscriptions
|
|
33
|
+
})
|
|
34
|
+
trackPageViewEvent = trackPageViewEventPlugin
|
|
35
|
+
|
|
36
|
+
jest.spyOn(destination, 'initialize').mockImplementation(() => {
|
|
37
|
+
mockHubspot = {
|
|
38
|
+
push: jest.fn()
|
|
39
|
+
}
|
|
40
|
+
return Promise.resolve(mockHubspot)
|
|
41
|
+
})
|
|
42
|
+
await trackPageViewEvent.load(Context.system(), {} as Analytics)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('call both trackPageView and setPath', async () => {
|
|
46
|
+
const context = new Context({
|
|
47
|
+
type: 'page',
|
|
48
|
+
name: 'Fried Chicken ๐',
|
|
49
|
+
category: 'Chicken Shop',
|
|
50
|
+
context: {
|
|
51
|
+
page: {
|
|
52
|
+
path: '/fried-chicken'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
await trackPageViewEvent.track?.(context)
|
|
57
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(2)
|
|
58
|
+
expect(mockHubspot.push).toHaveBeenCalledWith(['trackPageView'])
|
|
59
|
+
expect(mockHubspot.push).toHaveBeenCalledWith(['setPath', '/fried-chicken'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('only calls trackPageView if no path is provided', async () => {
|
|
63
|
+
const context = new Context({
|
|
64
|
+
type: 'page',
|
|
65
|
+
name: 'Spicy Chicken Sandwich ๐ถ',
|
|
66
|
+
category: 'Chicken Shop'
|
|
67
|
+
})
|
|
68
|
+
await trackPageViewEvent.track?.(context)
|
|
69
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(1)
|
|
70
|
+
expect(mockHubspot.push).toHaveBeenCalledWith(['trackPageView'])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
|
|
2
|
+
import type { Settings } from '../generated-types'
|
|
3
|
+
import { Hubspot } from '../types'
|
|
4
|
+
import type { Payload } from './generated-types'
|
|
5
|
+
|
|
6
|
+
const action: BrowserActionDefinition<Settings, Hubspot, Payload> = {
|
|
7
|
+
title: 'Track Page View',
|
|
8
|
+
description: 'Track the page view for the current page in HubSpot.',
|
|
9
|
+
defaultSubscription: 'type = "page"',
|
|
10
|
+
platform: 'web',
|
|
11
|
+
fields: {
|
|
12
|
+
path: {
|
|
13
|
+
description:
|
|
14
|
+
'The path of the current page. The set path will be treated as relative to the current domain being viewed.',
|
|
15
|
+
label: 'Path String',
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
perform: (_hsq, event) => {
|
|
21
|
+
if (event.payload.path) {
|
|
22
|
+
_hsq.push(['setPath', event.payload.path])
|
|
23
|
+
}
|
|
24
|
+
_hsq.push(['trackPageView'])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default action
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import { Subscription } from '@segment/browser-destination-runtime'
|
|
3
|
+
import hubspotDestination, { destination } from '../../index'
|
|
4
|
+
import { Hubspot } from '../../types'
|
|
5
|
+
|
|
6
|
+
const subscriptions: Subscription[] = [
|
|
7
|
+
{
|
|
8
|
+
partnerAction: 'upsertContact',
|
|
9
|
+
name: 'Upsert Contact',
|
|
10
|
+
enabled: true,
|
|
11
|
+
subscribe: 'type = "identify"',
|
|
12
|
+
mapping: {
|
|
13
|
+
email: {
|
|
14
|
+
'@path': '$.traits.email'
|
|
15
|
+
},
|
|
16
|
+
custom_properties: {
|
|
17
|
+
'@path': '$.traits'
|
|
18
|
+
},
|
|
19
|
+
id: {
|
|
20
|
+
'@path': '$.userId'
|
|
21
|
+
},
|
|
22
|
+
company: {
|
|
23
|
+
'@path': '$.traits.company.name'
|
|
24
|
+
},
|
|
25
|
+
country: {
|
|
26
|
+
'@path': '$.traits.address.country'
|
|
27
|
+
},
|
|
28
|
+
state: {
|
|
29
|
+
'@path': '$.traits.address.state'
|
|
30
|
+
},
|
|
31
|
+
city: {
|
|
32
|
+
'@path': '$.traits.address.city'
|
|
33
|
+
},
|
|
34
|
+
address: {
|
|
35
|
+
'@path': '$.traits.address.street'
|
|
36
|
+
},
|
|
37
|
+
zip: {
|
|
38
|
+
'@path': '$.traits.address.postalCode'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
describe('Hubspot.upsertContact', () => {
|
|
45
|
+
const settings = {
|
|
46
|
+
portalId: '1234',
|
|
47
|
+
formatCustomBehavioralEventNames: true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let mockHubspot: Hubspot
|
|
51
|
+
let upsertContactEvent: any
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
jest.restoreAllMocks()
|
|
54
|
+
|
|
55
|
+
const [upsertContactEventPlugin] = await hubspotDestination({
|
|
56
|
+
...settings,
|
|
57
|
+
subscriptions
|
|
58
|
+
})
|
|
59
|
+
upsertContactEvent = upsertContactEventPlugin
|
|
60
|
+
|
|
61
|
+
jest.spyOn(destination, 'initialize').mockImplementation(() => {
|
|
62
|
+
mockHubspot = {
|
|
63
|
+
push: jest.fn()
|
|
64
|
+
}
|
|
65
|
+
return Promise.resolve(mockHubspot)
|
|
66
|
+
})
|
|
67
|
+
await upsertContactEvent.load(Context.system(), {} as Analytics)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('does not call Hubspot if there is no email', async () => {
|
|
71
|
+
const context = new Context({
|
|
72
|
+
type: 'identify',
|
|
73
|
+
userId: '๐ป',
|
|
74
|
+
traits: {
|
|
75
|
+
friendly: true
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await upsertContactEvent.identify?.(context)
|
|
80
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('Identifies the user to Hubspot when email is present', async () => {
|
|
84
|
+
const context = new Context({
|
|
85
|
+
type: 'identify',
|
|
86
|
+
userId: 'real_hubspot_tester',
|
|
87
|
+
traits: {
|
|
88
|
+
friendly: false,
|
|
89
|
+
email: 'real_hubspot_tester@jest_experts.com'
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await upsertContactEvent.identify?.(context)
|
|
94
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(1)
|
|
95
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
96
|
+
'identify',
|
|
97
|
+
{ email: 'real_hubspot_tester@jest_experts.com', id: 'real_hubspot_tester', friendly: false }
|
|
98
|
+
])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('populates company info from the traits', async () => {
|
|
102
|
+
const context = new Context({
|
|
103
|
+
type: 'identify',
|
|
104
|
+
userId: 'mike',
|
|
105
|
+
traits: {
|
|
106
|
+
friendly: false,
|
|
107
|
+
email: 'mike_eh@lph.com',
|
|
108
|
+
company: {
|
|
109
|
+
id: '123',
|
|
110
|
+
name: 'Los Pollos Hermanos',
|
|
111
|
+
industry: 'Transportation',
|
|
112
|
+
employee_count: 128,
|
|
113
|
+
plan: 'startup'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await upsertContactEvent.identify?.(context)
|
|
119
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(1)
|
|
120
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
121
|
+
'identify',
|
|
122
|
+
{ email: 'mike_eh@lph.com', id: 'mike', friendly: false, company: 'Los Pollos Hermanos' }
|
|
123
|
+
])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('populates address info from the traits', async () => {
|
|
127
|
+
const context = new Context({
|
|
128
|
+
type: 'identify',
|
|
129
|
+
userId: 'mike',
|
|
130
|
+
traits: {
|
|
131
|
+
friendly: false,
|
|
132
|
+
email: 'mike_eh@lph.com',
|
|
133
|
+
address: {
|
|
134
|
+
street: '6th St',
|
|
135
|
+
city: 'San Francisco',
|
|
136
|
+
state: 'CA',
|
|
137
|
+
postalCode: '94103',
|
|
138
|
+
country: 'USA'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await upsertContactEvent.identify?.(context)
|
|
144
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(1)
|
|
145
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
146
|
+
'identify',
|
|
147
|
+
{
|
|
148
|
+
email: 'mike_eh@lph.com',
|
|
149
|
+
id: 'mike',
|
|
150
|
+
friendly: false,
|
|
151
|
+
address: '6th St',
|
|
152
|
+
country: 'USA',
|
|
153
|
+
state: 'CA',
|
|
154
|
+
city: 'San Francisco',
|
|
155
|
+
zip: '94103'
|
|
156
|
+
}
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('flattens nested traits', async () => {
|
|
161
|
+
const context = new Context({
|
|
162
|
+
type: 'identify',
|
|
163
|
+
userId: 'mike',
|
|
164
|
+
traits: {
|
|
165
|
+
friendly: false,
|
|
166
|
+
email: 'mike_eh@lph.com',
|
|
167
|
+
address: {
|
|
168
|
+
street: '6th St',
|
|
169
|
+
city: 'San Francisco',
|
|
170
|
+
state: 'CA',
|
|
171
|
+
postalCode: '94103',
|
|
172
|
+
country: 'USA'
|
|
173
|
+
},
|
|
174
|
+
equipment: {
|
|
175
|
+
type: '๐',
|
|
176
|
+
color: 'red',
|
|
177
|
+
make: {
|
|
178
|
+
make: 'Tesla',
|
|
179
|
+
model: 'Model S',
|
|
180
|
+
year: 2019
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await upsertContactEvent.identify?.(context)
|
|
187
|
+
expect(mockHubspot.push).toHaveBeenCalledTimes(1)
|
|
188
|
+
expect(mockHubspot.push).toHaveBeenCalledWith([
|
|
189
|
+
'identify',
|
|
190
|
+
{
|
|
191
|
+
email: 'mike_eh@lph.com',
|
|
192
|
+
id: 'mike',
|
|
193
|
+
friendly: false,
|
|
194
|
+
address: '6th St',
|
|
195
|
+
country: 'USA',
|
|
196
|
+
state: 'CA',
|
|
197
|
+
city: 'San Francisco',
|
|
198
|
+
zip: '94103',
|
|
199
|
+
equipment_type: '๐',
|
|
200
|
+
equipment_color: 'red',
|
|
201
|
+
equipment_make_make: 'Tesla',
|
|
202
|
+
equipment_make_model: 'Model S',
|
|
203
|
+
equipment_make_year: 2019
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Payload {
|
|
4
|
+
/**
|
|
5
|
+
* The contactโs email. Email is used to uniquely identify contact records in HubSpot and create or update the contact accordingly.
|
|
6
|
+
*/
|
|
7
|
+
email: string
|
|
8
|
+
/**
|
|
9
|
+
* A custom external ID that identifies the visitor.
|
|
10
|
+
*/
|
|
11
|
+
id?: string
|
|
12
|
+
/**
|
|
13
|
+
* A list of key-value pairs that describe the contact. Please see [HubSpot`s documentation](https://knowledge.hubspot.com/account/prevent-contact-properties-update-through-tracking-code-api) for limitations in updating contact properties.
|
|
14
|
+
*/
|
|
15
|
+
custom_properties?: {
|
|
16
|
+
[k: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The name of the company the contact is associated with.
|
|
20
|
+
*/
|
|
21
|
+
company?: string
|
|
22
|
+
/**
|
|
23
|
+
* The name of the country the contact is associated with.
|
|
24
|
+
*/
|
|
25
|
+
country?: string
|
|
26
|
+
/**
|
|
27
|
+
* The name of the state the contact is associated with.
|
|
28
|
+
*/
|
|
29
|
+
state?: string
|
|
30
|
+
/**
|
|
31
|
+
* The name of the city the contact is associated with.
|
|
32
|
+
*/
|
|
33
|
+
city?: string
|
|
34
|
+
/**
|
|
35
|
+
* The street address of the contact.
|
|
36
|
+
*/
|
|
37
|
+
address?: string
|
|
38
|
+
/**
|
|
39
|
+
* The postal code of the contact.
|
|
40
|
+
*/
|
|
41
|
+
zip?: string
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { Hubspot } from '../types'
|
|
5
|
+
import { flatten } from '../utils/flatten'
|
|
6
|
+
|
|
7
|
+
const action: BrowserActionDefinition<Settings, Hubspot, Payload> = {
|
|
8
|
+
title: 'Upsert Contact',
|
|
9
|
+
description: 'Create or update a contact in HubSpot.',
|
|
10
|
+
defaultSubscription: 'type = "identify"',
|
|
11
|
+
platform: 'web',
|
|
12
|
+
fields: {
|
|
13
|
+
email: {
|
|
14
|
+
description:
|
|
15
|
+
'The contactโs email. Email is used to uniquely identify contact records in HubSpot and create or update the contact accordingly.',
|
|
16
|
+
label: 'Email Address',
|
|
17
|
+
type: 'string',
|
|
18
|
+
required: true,
|
|
19
|
+
default: {
|
|
20
|
+
'@path': '$.traits.email'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
id: {
|
|
24
|
+
description: 'A custom external ID that identifies the visitor.',
|
|
25
|
+
label: 'External ID',
|
|
26
|
+
type: 'string',
|
|
27
|
+
required: false,
|
|
28
|
+
default: {
|
|
29
|
+
'@path': '$.userId'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
custom_properties: {
|
|
33
|
+
description: 'A list of key-value pairs that describe the contact. Please see [HubSpot`s documentation](https://knowledge.hubspot.com/account/prevent-contact-properties-update-through-tracking-code-api) for limitations in updating contact properties.',
|
|
34
|
+
label: 'Custom Properties',
|
|
35
|
+
type: 'object',
|
|
36
|
+
required: false,
|
|
37
|
+
default: {
|
|
38
|
+
'@path': '$.traits'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
company: {
|
|
42
|
+
description: 'The name of the company the contact is associated with.',
|
|
43
|
+
label: 'Company Name',
|
|
44
|
+
type: 'string',
|
|
45
|
+
required: false,
|
|
46
|
+
default: {
|
|
47
|
+
'@path': '$.traits.company.name'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
country: {
|
|
51
|
+
description: 'The name of the country the contact is associated with.',
|
|
52
|
+
label: 'Country',
|
|
53
|
+
type: 'string',
|
|
54
|
+
required: false,
|
|
55
|
+
default: {
|
|
56
|
+
'@path': '$.traits.address.country'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
state: {
|
|
60
|
+
description: 'The name of the state the contact is associated with.',
|
|
61
|
+
label: 'State',
|
|
62
|
+
type: 'string',
|
|
63
|
+
required: false,
|
|
64
|
+
default: {
|
|
65
|
+
'@path': '$.traits.address.state'
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
city: {
|
|
69
|
+
description: 'The name of the city the contact is associated with.',
|
|
70
|
+
label: 'City',
|
|
71
|
+
type: 'string',
|
|
72
|
+
required: false,
|
|
73
|
+
default: {
|
|
74
|
+
'@path': '$.traits.address.city'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
address: {
|
|
78
|
+
description: 'The street address of the contact.',
|
|
79
|
+
label: 'Street Address',
|
|
80
|
+
type: 'string',
|
|
81
|
+
required: false,
|
|
82
|
+
default: {
|
|
83
|
+
'@path': '$.traits.address.street'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
zip: {
|
|
87
|
+
description: 'The postal code of the contact.',
|
|
88
|
+
label: 'Postal Code',
|
|
89
|
+
type: 'string',
|
|
90
|
+
required: false,
|
|
91
|
+
default: {
|
|
92
|
+
'@if': {
|
|
93
|
+
exists: { '@path': '$.traits.address.postalCode' },
|
|
94
|
+
then: { '@path': '$.traits.address.postalCode' },
|
|
95
|
+
else: { '@path': '$.traits.address.postal_code' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
perform: (_hsq, event) => {
|
|
101
|
+
const payload = event.payload
|
|
102
|
+
if (!payload.email) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// custom properties should be key-value pairs of strings, therefore, filtering out any non-primitive
|
|
107
|
+
const { custom_properties, ...rest } = payload
|
|
108
|
+
let flattenProperties
|
|
109
|
+
if (custom_properties) {
|
|
110
|
+
flattenProperties = flatten(custom_properties, '', ['address', 'company'])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_hsq.push(['identify', { ...flattenProperties, ...rest }])
|
|
114
|
+
|
|
115
|
+
if (event.settings.flushIdentifyImmediately) {
|
|
116
|
+
_hsq.push(['trackPageView'])
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default action
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { JSONPrimitive } from '@segment/actions-core'
|
|
2
|
+
|
|
3
|
+
export type Properties = {
|
|
4
|
+
[k: string]: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type FlattenProperties = object & {
|
|
8
|
+
[k: string]: JSONPrimitive
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function flatten(
|
|
12
|
+
data: Properties,
|
|
13
|
+
prefix = '',
|
|
14
|
+
skipList: string[] = [],
|
|
15
|
+
keyTransformation = (input: string) => input
|
|
16
|
+
): FlattenProperties {
|
|
17
|
+
let result: FlattenProperties = {}
|
|
18
|
+
for (const key in data) {
|
|
19
|
+
// skips flattening specific keys on the top level
|
|
20
|
+
if (!prefix && skipList.includes(key)) continue
|
|
21
|
+
|
|
22
|
+
if (typeof data[key] === 'object' && data[key] !== null) {
|
|
23
|
+
const flattened = flatten(data[key] as Properties, `${prefix}_${key}`, skipList, keyTransformation)
|
|
24
|
+
result = { ...result, ...flattened }
|
|
25
|
+
} else {
|
|
26
|
+
result[keyTransformation(`${prefix}_${key}`.replace(/^_/, ''))] = data[key] as JSONPrimitive
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
}
|