@segment/analytics-browser-actions-intercom 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.
@@ -0,0 +1,237 @@
1
+ import { InputField } from '@segment/actions-core'
2
+ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
3
+ import { Intercom } from '../api'
4
+ import type { Settings } from '../generated-types'
5
+ import { getCompanyProperties } from '../sharedCompanyProperties'
6
+ import { convertDateToUnix, filterCustomTraits, getWidgetOptions, isEmpty } from '../utils'
7
+ import type { Payload } from './generated-types'
8
+
9
+ const companyProperties: Record<string, InputField> = getCompanyProperties()
10
+
11
+ const action: BrowserActionDefinition<Settings, Intercom, Payload> = {
12
+ title: 'Identify User',
13
+ description: 'Create or update a user in Intercom.',
14
+ defaultSubscription: 'type = "identify" or type = "page"',
15
+ platform: 'web',
16
+ fields: {
17
+ user_id: {
18
+ description: 'A unique identifier for the user.',
19
+ label: 'User ID',
20
+ type: 'string',
21
+ required: false,
22
+ default: {
23
+ '@path': '$.userId'
24
+ }
25
+ },
26
+ custom_traits: {
27
+ description: "The user's custom attributes.",
28
+ label: 'Custom Attributes',
29
+ type: 'object',
30
+ required: false,
31
+ defaultObjectUI: 'keyvalue'
32
+ },
33
+ name: {
34
+ description: "The user's name.",
35
+ label: 'Name',
36
+ type: 'string',
37
+ required: false,
38
+ default: {
39
+ '@path': '$.traits.name'
40
+ }
41
+ },
42
+ phone: {
43
+ description: "The user's phone number.",
44
+ label: 'Phone Number',
45
+ type: 'string',
46
+ required: false,
47
+ default: {
48
+ '@path': '$.traits.phone'
49
+ }
50
+ },
51
+ unsubscribed_from_emails: {
52
+ description: "The user's email unsubscribe status.",
53
+ label: 'Unsubscribed From Emails',
54
+ type: 'boolean',
55
+ required: false
56
+ },
57
+ language_override: {
58
+ description: "The user's messenger language (instead of relying on browser language settings).",
59
+ label: 'Language Override',
60
+ type: 'string',
61
+ required: false
62
+ },
63
+ email: {
64
+ description: "The user's email address.",
65
+ label: 'Email Address',
66
+ type: 'string',
67
+ required: false,
68
+ default: {
69
+ '@path': '$.traits.email'
70
+ }
71
+ },
72
+ created_at: {
73
+ description: 'The time the user was created in your system.',
74
+ label: 'User Creation Time',
75
+ type: 'datetime',
76
+ required: false,
77
+ default: {
78
+ '@if': {
79
+ exists: { '@path': '$.traits.createdAt' },
80
+ then: { '@path': '$.traits.createdAt' },
81
+ else: { '@path': '$.traits.created_at' }
82
+ }
83
+ }
84
+ },
85
+ avatar_image_url: {
86
+ description: "The URL for the user's avatar/profile image.",
87
+ label: 'Avatar',
88
+ type: 'string',
89
+ required: false,
90
+ default: { '@path': '$.traits.avatar' }
91
+ },
92
+ user_hash: {
93
+ description:
94
+ 'The user hash used for identity verification. See [Intercom docs](https://www.intercom.com/help/en/articles/183-enable-identity-verification-for-web-and-mobile) for more information on how to set this field.',
95
+ label: 'User Hash',
96
+ type: 'string',
97
+ required: false,
98
+ default: {
99
+ '@if': {
100
+ exists: { '@path': '$.context.Intercom.user_hash' },
101
+ then: { '@path': '$.context.Intercom.user_hash' },
102
+ else: { '@path': '$.context.Intercom.userHash' }
103
+ }
104
+ }
105
+ },
106
+ company: {
107
+ description: "The user's company.",
108
+ label: 'Company',
109
+ type: 'object',
110
+ required: false,
111
+ properties: companyProperties,
112
+ default: {
113
+ company_id: { '@path': '$.traits.company.id' },
114
+ name: { '@path': '$.traits.company.name' },
115
+ created_at: {
116
+ '@if': {
117
+ exists: { '@path': '$.traits.company.createdAt' },
118
+ then: { '@path': '$.traits.company.createdAt' },
119
+ else: { '@path': '$.traits.company.created_at' }
120
+ }
121
+ },
122
+ plan: { '@path': '$.traits.company.plan' },
123
+ size: { '@path': '$.traits.company.size' },
124
+ website: { '@path': '$.traits.company.website' },
125
+ industry: { '@path': '$.traits.company.industry' },
126
+ monthly_spend: { '@path': '$.traits.company.monthly_spend' }
127
+ }
128
+ },
129
+ companies: {
130
+ description: 'The array of companies the user is associated to.',
131
+ label: 'Companies',
132
+ type: 'object',
133
+ multiple: true,
134
+ required: false,
135
+ properties: companyProperties,
136
+ default: {
137
+ '@arrayPath': [
138
+ '$.traits.companies',
139
+ {
140
+ company_id: { '@path': '$.id' },
141
+ name: { '@path': '$.name' },
142
+ created_at: {
143
+ '@if': {
144
+ exists: { '@path': '$.createdAt' },
145
+ then: { '@path': '$.createdAt' },
146
+ else: { '@path': '$.created_at' }
147
+ }
148
+ },
149
+ plan: { '@path': '$.plan' },
150
+ size: { '@path': '$.size' },
151
+ website: { '@path': '$.website' },
152
+ industry: { '@path': '$.industry' },
153
+ monthly_spend: { '@path': '$.monthly_spend' }
154
+ }
155
+ ]
156
+ }
157
+ },
158
+ hide_default_launcher: {
159
+ description:
160
+ 'Selectively show the chat widget. As per [Intercom docs](https://www.intercom.com/help/en/articles/189-turn-off-show-or-hide-the-intercom-messenger), you want to first hide the Messenger for all users inside the Intercom UI using Messenger settings. Then think about how you want to programmatically decide which users you would like to show the widget to.',
161
+ label: 'Hide Default Launcher',
162
+ type: 'boolean',
163
+ required: false,
164
+ default: {
165
+ '@if': {
166
+ exists: { '@path': '$.context.Intercom.hideDefaultLauncher' },
167
+ then: { '@path': '$.context.Intercom.hideDefaultLauncher' },
168
+ else: { '@path': '$.context.Intercom.hide_default_launcher' }
169
+ }
170
+ }
171
+ }
172
+ },
173
+ perform: (Intercom, event) => {
174
+ // remove properties that require extra handling
175
+ const { custom_traits, avatar_image_url, ...rest } = event.payload
176
+ const payload = { ...rest }
177
+
178
+ // remove company if it is empty
179
+ if (isEmpty(payload.company?.company_custom_traits)) {
180
+ delete payload.company?.company_custom_traits
181
+ }
182
+ if (isEmpty(payload.company)) {
183
+ delete payload.company
184
+ }
185
+
186
+ // convert 'created_at' date properties from ISO-8601 to UNIX
187
+ const companies = Array.isArray(payload.companies) ? [...payload.companies] : []
188
+ const datesToConvert = [payload, payload.company, ...companies]
189
+ for (const objectWithDateProp of datesToConvert) {
190
+ if (objectWithDateProp && objectWithDateProp?.created_at) {
191
+ objectWithDateProp.created_at = convertDateToUnix(objectWithDateProp.created_at)
192
+ }
193
+ }
194
+
195
+ // drop custom objects & arrays
196
+ const filteredCustomTraits = filterCustomTraits(custom_traits)
197
+
198
+ // drop custom objects & arrays
199
+ if (payload.company) {
200
+ const { company_custom_traits, ...rest } = payload.company
201
+ const companyFilteredCustomTraits = filterCustomTraits(company_custom_traits)
202
+ payload.company = { ...rest, ...companyFilteredCustomTraits }
203
+ }
204
+
205
+ // drop custom objects & arrays
206
+ if (payload.companies) {
207
+ payload.companies = payload.companies.map((company) => {
208
+ const { company_custom_traits, ...rest } = company
209
+ const companyFilteredCustomTraits = filterCustomTraits(company_custom_traits)
210
+ company = { ...rest, ...companyFilteredCustomTraits }
211
+ return company
212
+ })
213
+ }
214
+
215
+ // get user's widget options
216
+ const widgetOptions = getWidgetOptions(payload.hide_default_launcher, Intercom.activator)
217
+
218
+ // create the avatar object
219
+ let avatar = {}
220
+ if (avatar_image_url) {
221
+ avatar = {
222
+ image_url: avatar_image_url,
223
+ type: 'avatar'
224
+ }
225
+ }
226
+
227
+ // API call
228
+ Intercom('update', {
229
+ ...payload,
230
+ ...filteredCustomTraits,
231
+ ...widgetOptions,
232
+ ...(!isEmpty(avatar) && { avatar })
233
+ })
234
+ }
235
+ }
236
+
237
+ export default action
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
+ import { initialBoot, initScript } from './init-script'
5
+
6
+ import { Intercom } from './api'
7
+ import trackEvent from './trackEvent'
8
+ import identifyUser from './identifyUser'
9
+ import identifyCompany from './identifyCompany'
10
+ import { defaultValues } from '@segment/actions-core'
11
+
12
+ declare global {
13
+ interface Window {
14
+ Intercom: Intercom
15
+ }
16
+ }
17
+
18
+ export const destination: BrowserDestinationDefinition<Settings, Intercom> = {
19
+ name: 'Intercom Web (Actions)',
20
+ slug: 'actions-intercom-web',
21
+ mode: 'device',
22
+ presets: [
23
+ {
24
+ name: 'Track Event',
25
+ subscribe: 'type = "track"',
26
+ partnerAction: 'trackEvent',
27
+ mapping: defaultValues(trackEvent.fields)
28
+ },
29
+ {
30
+ name: 'Identify User',
31
+ subscribe: 'type = "identify" or type = "page"',
32
+ partnerAction: 'identifyUser',
33
+ mapping: defaultValues(identifyUser.fields)
34
+ },
35
+ {
36
+ name: 'Identify Company',
37
+ subscribe: 'type = "group"',
38
+ partnerAction: 'identifyCompany',
39
+ mapping: defaultValues(identifyCompany.fields)
40
+ }
41
+ ],
42
+ settings: {
43
+ appId: {
44
+ description: 'The app_id of your Intercom app which will indicate where to store any data.',
45
+ label: 'App ID',
46
+ type: 'string',
47
+ required: true
48
+ },
49
+ activator: {
50
+ description:
51
+ 'By default, Intercom will inject their own inbox button onto the page, but you can choose to use your own custom button instead by providing a CSS selector, e.g. #my-button. You must have the "Show the Intercom Inbox" setting enabled for this to work. The default value is #IntercomDefaultWidget.',
52
+ label: 'Custom Inbox Button Selector',
53
+ type: 'string',
54
+ required: false,
55
+ default: '#IntercomDefaultWidget'
56
+ },
57
+ richLinkProperties: {
58
+ description: 'A list of rich link property keys.',
59
+ label: 'Rich Link Properties',
60
+ type: 'string',
61
+ multiple: true,
62
+ required: false
63
+ },
64
+ apiBase: {
65
+ description: 'The regional API to use for processing the data',
66
+ label: 'Regional Data Hosting',
67
+ type: 'string',
68
+ choices: [
69
+ {
70
+ label: 'US',
71
+ value: 'https://api-iam.intercom.io'
72
+ },
73
+ {
74
+ label: 'EU',
75
+ value: 'https://api-iam.eu.intercom.io'
76
+ },
77
+ {
78
+ label: 'Australia',
79
+ value: 'https://api-iam.au.intercom.io'
80
+ }
81
+ ],
82
+ default: 'https://api-iam.intercom.io',
83
+ required: false
84
+ }
85
+ },
86
+
87
+ initialize: async ({ settings }, deps) => {
88
+ //initialize Intercom
89
+ initScript({ appId: settings.appId })
90
+ const preloadedIntercom = window.Intercom
91
+ initialBoot(settings.appId, { api_base: settings.apiBase })
92
+
93
+ await deps.resolveWhen(() => window.Intercom !== preloadedIntercom, 100)
94
+
95
+ window.Intercom.richLinkProperties = settings.richLinkProperties
96
+ window.Intercom.appId = settings.appId
97
+ window.Intercom.activator = settings.activator
98
+
99
+ return window.Intercom
100
+ },
101
+
102
+ actions: {
103
+ trackEvent,
104
+ identifyUser,
105
+ identifyCompany
106
+ }
107
+ }
108
+
109
+ export default browserDestination(destination)
@@ -0,0 +1,49 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+
4
+ export function initScript({ appId }) {
5
+ //Set your APP_ID
6
+ var APP_ID = appId
7
+ ;(function () {
8
+ var w = window
9
+ var ic = w.Intercom
10
+ if (typeof ic === 'function') {
11
+ ic('reattach_activator')
12
+ ic('update', w.intercomSettings)
13
+ } else {
14
+ var d = document
15
+ var i = function () {
16
+ i.c(arguments)
17
+ }
18
+ i.q = []
19
+ i.c = function (args) {
20
+ i.q.push(args)
21
+ }
22
+ w.Intercom = i
23
+ var l = function () {
24
+ var s = d.createElement('script')
25
+ s.type = 'text/javascript'
26
+ s.async = true
27
+ s.src = 'https://widget.intercom.io/widget/' + APP_ID
28
+ var x = d.getElementsByTagName('script')[0]
29
+ x.parentNode.insertBefore(s, x)
30
+ }
31
+ if (document.readyState === 'complete') {
32
+ l()
33
+ } else if (w.attachEvent) {
34
+ w.attachEvent('onload', l)
35
+ } else {
36
+ w.addEventListener('load', l, false)
37
+ }
38
+ }
39
+ })()
40
+ }
41
+
42
+ export function initialBoot(appId: string, options = {}) {
43
+ window &&
44
+ window.Intercom &&
45
+ window.Intercom('boot', {
46
+ app_id: appId,
47
+ ...options
48
+ })
49
+ }
@@ -0,0 +1,59 @@
1
+ import { InputField } from '@segment/actions-core'
2
+
3
+ export const getCompanyProperties = (): Record<string, InputField> => ({
4
+ company_id: {
5
+ description: 'The unique identifier of the company.',
6
+ label: 'Company ID',
7
+ type: 'string',
8
+ required: true
9
+ },
10
+ name: {
11
+ description: 'The name of the company.',
12
+ label: 'Company Name',
13
+ type: 'string',
14
+ required: true
15
+ },
16
+ created_at: {
17
+ description: 'The time the company was created in your system.',
18
+ label: 'Company Creation Time',
19
+ type: 'datetime',
20
+ required: false
21
+ },
22
+ plan: {
23
+ description: 'The name of the plan you have associated with the company.',
24
+ label: 'Company Plan',
25
+ type: 'string',
26
+ required: false
27
+ },
28
+ monthly_spend: {
29
+ description: 'The monthly spend of the company, e.g. how much revenue the company generates for your business.',
30
+ label: 'Monthly Spend',
31
+ type: 'integer',
32
+ required: false
33
+ },
34
+ size: {
35
+ description: 'The number of employees in the company.',
36
+ label: 'Company Size',
37
+ type: 'integer',
38
+ required: false
39
+ },
40
+ website: {
41
+ description: 'The URL for the company website.',
42
+ label: 'Company Website',
43
+ type: 'string',
44
+ required: false
45
+ },
46
+ industry: {
47
+ description: 'The industry that the company operates in.',
48
+ label: 'Industry',
49
+ type: 'string',
50
+ required: false
51
+ },
52
+ company_custom_traits: {
53
+ description: 'The custom attributes for the company object.',
54
+ label: 'Company Custom Attributes',
55
+ type: 'object',
56
+ required: false,
57
+ defaultObjectUI: 'keyvalue'
58
+ }
59
+ })
@@ -0,0 +1,179 @@
1
+ import { Analytics, Context } from '@segment/analytics-next'
2
+ import { Subscription } from '@segment/browser-destination-runtime'
3
+ import intercomDestination, { destination } from '../../index'
4
+
5
+ const subscriptions: Subscription[] = [
6
+ {
7
+ partnerAction: 'trackEvent',
8
+ name: 'Show',
9
+ enabled: true,
10
+ subscribe: 'type = "track"',
11
+ mapping: {
12
+ event_name: {
13
+ '@path': '$.event'
14
+ },
15
+ event_metadata: {
16
+ '@path': '$.properties'
17
+ },
18
+ revenue: {
19
+ '@path': '$.properties.revenue'
20
+ },
21
+ currency: {
22
+ '@path': '$.properties.currency'
23
+ }
24
+ }
25
+ }
26
+ ]
27
+
28
+ describe('Intercom.trackEvent', () => {
29
+ const settings = {
30
+ appId: 'superSecretAppID'
31
+ }
32
+
33
+ let mockIntercom: jest.Mock<any, any>
34
+ let trackEvent: any
35
+ beforeEach(async () => {
36
+ jest.restoreAllMocks()
37
+
38
+ const [trackEventPlugin] = await intercomDestination({
39
+ ...settings,
40
+ subscriptions
41
+ })
42
+ trackEvent = trackEventPlugin
43
+
44
+ mockIntercom = jest.fn()
45
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
46
+ const mockedWithProps = Object.assign(mockIntercom as any, settings)
47
+ return Promise.resolve(mockedWithProps)
48
+ })
49
+ await trackEvent.load(Context.system(), {} as Analytics)
50
+ })
51
+
52
+ test('maps custom traits correctly', async () => {
53
+ const context = new Context({
54
+ type: 'track',
55
+ event: 'surfboard-bought',
56
+ properties: {
57
+ surfer: 'kelly slater'
58
+ }
59
+ })
60
+ await trackEvent.track?.(context)
61
+
62
+ expect(mockIntercom).toHaveBeenCalledWith('trackEvent', 'surfboard-bought', {
63
+ surfer: 'kelly slater'
64
+ })
65
+ })
66
+
67
+ test('maps price correctly', async () => {
68
+ const context = new Context({
69
+ type: 'track',
70
+ event: 'surfboard-bought',
71
+ properties: {
72
+ revenue: 100,
73
+ currency: 'USD'
74
+ }
75
+ })
76
+ await trackEvent.track?.(context)
77
+
78
+ expect(mockIntercom).toHaveBeenCalledWith('trackEvent', 'surfboard-bought', {
79
+ price: {
80
+ amount: 10000,
81
+ currency: 'USD'
82
+ }
83
+ })
84
+ })
85
+
86
+ test('currency defaults to USD if omitted', async () => {
87
+ const context = new Context({
88
+ type: 'track',
89
+ event: 'surfboard-bought',
90
+ properties: {
91
+ revenue: 100
92
+ }
93
+ })
94
+ await trackEvent.track?.(context)
95
+
96
+ expect(mockIntercom).toHaveBeenCalledWith('trackEvent', 'surfboard-bought', {
97
+ price: {
98
+ amount: 10000,
99
+ currency: 'USD'
100
+ }
101
+ })
102
+ })
103
+
104
+ test('drops arrays or objects in properties', async () => {
105
+ const context = new Context({
106
+ type: 'track',
107
+ event: 'surfboard-bought',
108
+ properties: {
109
+ surfer: 'kelly slater',
110
+ dropMe: {
111
+ foo: 'bar',
112
+ ahoy: {
113
+ okay: 'hello'
114
+ }
115
+ },
116
+ arr: ['hi', 'sup', 'yo']
117
+ }
118
+ })
119
+
120
+ await trackEvent.track?.(context)
121
+
122
+ expect(mockIntercom).toHaveBeenCalledWith('trackEvent', 'surfboard-bought', {
123
+ surfer: 'kelly slater'
124
+ })
125
+ })
126
+ })
127
+
128
+ describe('Intercom.trackEvent with rich link properties', () => {
129
+ const settings = {
130
+ appId: 'superSecretAppID',
131
+ richLinkProperties: ['article']
132
+ }
133
+
134
+ let mockIntercom: jest.Mock<any, any>
135
+ let trackEvent: any
136
+ beforeEach(async () => {
137
+ jest.restoreAllMocks()
138
+
139
+ const [trackEventPlugin] = await intercomDestination({
140
+ ...settings,
141
+ subscriptions
142
+ })
143
+ trackEvent = trackEventPlugin
144
+
145
+ mockIntercom = jest.fn()
146
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
147
+ const mockedWithProps = Object.assign(mockIntercom as any, settings)
148
+ return Promise.resolve(mockedWithProps)
149
+ })
150
+ await trackEvent.load(Context.system(), {} as Analytics)
151
+ })
152
+
153
+ test('rich link properties are permitted', async () => {
154
+ const context = new Context({
155
+ type: 'track',
156
+ event: 'surfboard-bought',
157
+ properties: {
158
+ surfer: 'kelly slater',
159
+ dropMe: {
160
+ foo: 'bar'
161
+ },
162
+ article: {
163
+ url: 'im a link',
164
+ value: 'hi'
165
+ }
166
+ }
167
+ })
168
+
169
+ await trackEvent.track?.(context)
170
+
171
+ expect(mockIntercom).toHaveBeenCalledWith('trackEvent', 'surfboard-bought', {
172
+ surfer: 'kelly slater',
173
+ article: {
174
+ url: 'im a link',
175
+ value: 'hi'
176
+ }
177
+ })
178
+ })
179
+ })
@@ -0,0 +1,22 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The name of the event.
6
+ */
7
+ event_name: string
8
+ /**
9
+ * The amount associated with a purchase. Segment will multiply by 100 as Intercom requires the amount in cents.
10
+ */
11
+ revenue?: number
12
+ /**
13
+ * The currency of the purchase amount. Segment will default to USD if revenue is provided without a currency.
14
+ */
15
+ currency?: string
16
+ /**
17
+ * Optional metadata describing the event.
18
+ */
19
+ event_metadata?: {
20
+ [k: string]: unknown
21
+ }
22
+ }