@segment/analytics-browser-actions-friendbuy 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,63 @@
1
+ import { Analytics, Context } from '@segment/analytics-next'
2
+ import friendbuyDestination from '../../index'
3
+ import trackPageObject, { trackPageDefaultSubscription, trackPageFields } from '../index'
4
+
5
+ import { loadScript } from '@segment/browser-destination-runtime/load-script'
6
+ jest.mock('@segment/browser-destination-runtime/load-script')
7
+ beforeEach(async () => {
8
+ // Prevent friendbuy.js and campaigns.js from being loaded.
9
+ ;(loadScript as jest.Mock).mockResolvedValue(true)
10
+ })
11
+
12
+ describe('Friendbuy.trackPage', () => {
13
+ const subscriptions = [
14
+ {
15
+ partnerAction: 'trackPage',
16
+ name: trackPageObject.title,
17
+ enabled: true,
18
+ subscribe: trackPageDefaultSubscription,
19
+ mapping: Object.fromEntries(Object.entries(trackPageFields).map(([name, value]) => [name, value.default]))
20
+ }
21
+ ]
22
+
23
+ test('all fields', async () => {
24
+ const merchantId = '1993d0f1-8206-4336-8c88-64e170f2419e'
25
+ const name = 'Page Name'
26
+ const category = 'Page Category'
27
+ const title = 'Page Title'
28
+
29
+ const [trackPage] = await friendbuyDestination({
30
+ merchantId,
31
+ subscriptions
32
+ })
33
+ expect(trackPage).toBeDefined()
34
+
35
+ await trackPage.load(Context.system(), {} as Analytics)
36
+
37
+ jest.spyOn(window.friendbuyAPI as any, 'push')
38
+
39
+ const context = new Context({
40
+ type: 'page',
41
+ name,
42
+ category,
43
+ properties: {
44
+ title
45
+ }
46
+ })
47
+ // console.log('context', JSON.stringify(context, null, 2))
48
+
49
+ trackPage.page?.(context)
50
+
51
+ // console.log('trackSignUp request', JSON.stringify(window.friendbuyAPI.push.mock.calls[0], null, 2))
52
+ expect(window.friendbuyAPI?.push).toHaveBeenCalledWith([
53
+ 'track',
54
+ 'page',
55
+ {
56
+ name,
57
+ category,
58
+ title
59
+ },
60
+ true
61
+ ])
62
+ })
63
+ })
@@ -0,0 +1,16 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The page name.
6
+ */
7
+ name?: string
8
+ /**
9
+ * The page category.
10
+ */
11
+ category?: string
12
+ /**
13
+ * The page title.
14
+ */
15
+ title?: string
16
+ }
@@ -0,0 +1,58 @@
1
+ import type { InputField } from '@segment/actions-core'
2
+ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
3
+
4
+ import type { FriendbuyAPI } from '../types'
5
+ import type { Settings } from '../generated-types'
6
+ import type { Payload } from './generated-types'
7
+ import { createFriendbuyPayload } from '@segment/actions-shared'
8
+
9
+ export const trackPageDefaultSubscription = 'type = "page"'
10
+
11
+ // https://segment.com/docs/connections/spec/page/
12
+ export const trackPageFields: Record<string, InputField> = {
13
+ name: {
14
+ label: 'Page Name',
15
+ description: 'The page name.',
16
+ type: 'string',
17
+ required: false,
18
+ default: { '@path': '$.name' }
19
+ },
20
+ category: {
21
+ label: 'Page Category',
22
+ description: 'The page category.',
23
+ type: 'string',
24
+ required: false,
25
+ default: { '@path': '$.category' }
26
+ },
27
+ title: {
28
+ label: 'Page Title',
29
+ description: 'The page title.',
30
+ type: 'string',
31
+ required: false,
32
+ default: { '@path': '$.properties.title' }
33
+ }
34
+ }
35
+
36
+ const action: BrowserActionDefinition<Settings, FriendbuyAPI, Payload> = {
37
+ title: 'Track Page',
38
+ description:
39
+ 'Record when a customer visits a new page. Allow Friendbuy widget targeting by Page Name instead of URL.',
40
+ defaultSubscription: trackPageDefaultSubscription,
41
+ platform: 'web',
42
+ fields: trackPageFields,
43
+
44
+ perform: (friendbuyAPI, data) => {
45
+ // If the page name is not defined then track the page with the name
46
+ // "undefined". This is intended to allow merchants to target their widgets
47
+ // using page names when not all of their `analytics.page` calls include the
48
+ // page name.
49
+ const friendbuyPayload = createFriendbuyPayload([
50
+ ['name', data.payload.name || 'undefined'],
51
+ ['category', data.payload.category],
52
+ ['title', data.payload.title]
53
+ ])
54
+ friendbuyAPI.push(['track', 'page', friendbuyPayload, true])
55
+ }
56
+ }
57
+
58
+ export default action
@@ -0,0 +1,211 @@
1
+ import { Analytics, Context, JSONValue } from '@segment/analytics-next'
2
+ import friendbuyDestination from '../../index'
3
+ import trackPurchaseObject, { browserTrackPurchaseFields, trackPurchaseDefaultSubscription } from '../index'
4
+
5
+ import { loadScript } from '@segment/browser-destination-runtime/load-script'
6
+ jest.mock('@segment/browser-destination-runtime/load-script')
7
+ beforeEach(async () => {
8
+ // Prevent friendbuy.js and campaigns.js from being loaded.
9
+ ;(loadScript as jest.Mock).mockResolvedValue(true)
10
+ })
11
+
12
+ describe('Friendbuy.trackPurchase', () => {
13
+ const subscriptions = [
14
+ {
15
+ partnerAction: 'trackPurchase',
16
+ name: trackPurchaseObject.title,
17
+ enabled: true,
18
+ subscribe: trackPurchaseDefaultSubscription,
19
+ mapping: Object.fromEntries(
20
+ Object.entries(browserTrackPurchaseFields).map(([name, value]) => [name, value.default])
21
+ )
22
+ }
23
+ ]
24
+
25
+ test('all fields', async () => {
26
+ const orderId = 'my order'
27
+ const products = [
28
+ { sku: 'sku1', name: 'shorts', price: 19.99, quantity: 2 },
29
+ { price: 5.99 },
30
+ {
31
+ sku: 'sku3',
32
+ name: 'tshirt',
33
+ price: 24.99,
34
+ description: 'Black T-Shirt',
35
+ category: 'shirts',
36
+ url: 'https://example.com/sku3',
37
+ image_url: 'https://example.com/sku3/image.jpg'
38
+ }
39
+ ]
40
+
41
+ const merchantId = '1993d0f1-8206-4336-8c88-64e170f2419e'
42
+ const userId = 'john-doe-12345'
43
+ const anonymousId = 'cbce64f6-a45a-4d9c-a63d-4c7b42773276'
44
+ const currency = 'USD'
45
+ const coupon = 'coupon-xyzzy'
46
+ const attributionId = '878123ed-0439-4a73-b2fe-72c4f09ec64f'
47
+ const referralCode = 'ref-plugh'
48
+ const giftCardCodes = ['card-a', 'card-b']
49
+ const friendbuyAttributes = { promotion: 'fall 2021' }
50
+ const email = 'john.doe@example.com'
51
+ const name = 'John Doe'
52
+
53
+ const [trackPurchase] = await friendbuyDestination({
54
+ merchantId,
55
+ subscriptions
56
+ })
57
+ // console.log('trackPurchase', JSON.stringify(trackPurchase, null, 2), trackPurchase)
58
+ expect(trackPurchase).toBeDefined()
59
+
60
+ await trackPurchase.load(Context.system(), {} as Analytics)
61
+
62
+ // console.log(window.friendbuyAPI)
63
+ jest.spyOn(window.friendbuyAPI as any, 'push')
64
+
65
+ const expectedProducts = products.map((p) => {
66
+ p = { sku: 'unknown', name: 'unknown', quantity: 1, ...p }
67
+ if (p.image_url) {
68
+ p.imageUrl = p.image_url
69
+ delete p.image_url
70
+ }
71
+ return p
72
+ })
73
+ const amount = expectedProducts.reduce((acc, p) => acc + p.price * p.quantity, 0)
74
+
75
+ {
76
+ // all fields
77
+ const context1 = new Context({
78
+ type: 'track',
79
+ event: 'Order Completed',
80
+ userId,
81
+ anonymousId,
82
+ properties: {
83
+ order_id: orderId,
84
+ revenue: amount,
85
+ subtotal: amount + 1,
86
+ total: amount + 2,
87
+ currency,
88
+ coupon,
89
+ attributionId,
90
+ referralCode,
91
+ giftCardCodes,
92
+ products: products as JSONValue,
93
+ email,
94
+ name,
95
+ friendbuyAttributes
96
+ }
97
+ })
98
+ // console.log('context1', JSON.stringify(context1, null, 2))
99
+
100
+ trackPurchase.track?.(context1)
101
+
102
+ // console.log('trackPurchase request', JSON.stringify(window.friendbuyAPI.push.mock.calls[0], null, 2))
103
+ expect(window.friendbuyAPI?.push).toHaveBeenNthCalledWith(1, [
104
+ 'track',
105
+ 'purchase',
106
+ {
107
+ id: orderId,
108
+ amount: amount + 2, // amount defaults to total
109
+ currency,
110
+ couponCode: coupon,
111
+ attributionId,
112
+ referralCode,
113
+ giftCardCodes,
114
+ customer: { id: userId, anonymousId, email, name },
115
+ products: expectedProducts,
116
+ ...friendbuyAttributes
117
+ },
118
+ true
119
+ ])
120
+ expect(window.friendbuyAPI.push.mock.calls[0][0][2].products[1].quantity).toBe(1)
121
+ expect(window.friendbuyAPI.push.mock.calls[0][0][2].products[2].imageUrl).toBe(products[2].image_url)
122
+ }
123
+
124
+ {
125
+ // minimal event
126
+ const context2 = new Context({
127
+ type: 'track',
128
+ event: 'Order Completed',
129
+ properties: {
130
+ order_id: orderId,
131
+ total: amount,
132
+ currency
133
+ }
134
+ })
135
+
136
+ trackPurchase.track?.(context2)
137
+
138
+ expect(window.friendbuyAPI?.push).toHaveBeenNthCalledWith(2, [
139
+ 'track',
140
+ 'purchase',
141
+ {
142
+ id: orderId,
143
+ amount,
144
+ currency
145
+ },
146
+ true
147
+ ])
148
+ }
149
+
150
+ {
151
+ // customer is dropped if no userId/customerId
152
+ // giftCardCodes is dropped if list is empty
153
+ const context3 = new Context({
154
+ type: 'track',
155
+ event: 'Order Completed',
156
+ properties: {
157
+ order_id: orderId,
158
+ total: amount,
159
+ currency,
160
+ giftCardCodes: [],
161
+ email,
162
+ name
163
+ }
164
+ })
165
+
166
+ trackPurchase.track?.(context3)
167
+
168
+ expect(window.friendbuyAPI?.push).toHaveBeenNthCalledWith(3, [
169
+ 'track',
170
+ 'purchase',
171
+ {
172
+ id: orderId,
173
+ amount,
174
+ currency
175
+ },
176
+ true
177
+ ])
178
+ }
179
+
180
+ {
181
+ // enjoined fields are converted
182
+ const context4 = new Context({
183
+ type: 'track',
184
+ event: 'Order Completed',
185
+ properties: {
186
+ order_id: 12345,
187
+ total: '129.50',
188
+ currency,
189
+ products: [{ sku: 99999, quantity: '2', price: '64.75' }],
190
+ customerId: 1138,
191
+ age: '30'
192
+ }
193
+ })
194
+
195
+ trackPurchase.track?.(context4)
196
+
197
+ expect(window.friendbuyAPI?.push).toHaveBeenNthCalledWith(4, [
198
+ 'track',
199
+ 'purchase',
200
+ {
201
+ id: '12345',
202
+ amount: 129.5,
203
+ currency,
204
+ products: [{ name: 'unknown', sku: '99999', quantity: 2, price: 64.75 }],
205
+ customer: { id: '1138', age: 30 }
206
+ },
207
+ true
208
+ ])
209
+ }
210
+ })
211
+ })
@@ -0,0 +1,91 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The order ID.
6
+ */
7
+ orderId: string
8
+ /**
9
+ * Purchase amount to be considered when evaluating reward rules.
10
+ */
11
+ amount: number
12
+ /**
13
+ * The currency of the purchase amount.
14
+ */
15
+ currency: string
16
+ /**
17
+ * The coupon code of any coupon redeemed with the order.
18
+ */
19
+ coupon?: string
20
+ /**
21
+ * Friendbuy attribution ID that associates the purchase with the advocate who referred the purchaser.
22
+ */
23
+ attributionId?: string
24
+ /**
25
+ * Friendbuy referral code that associates the purchase with the advocate who referred the purchaser.
26
+ */
27
+ referralCode?: string
28
+ /**
29
+ * An array of gift card codes applied to the order.
30
+ */
31
+ giftCardCodes?: string[]
32
+ /**
33
+ * Products purchased.
34
+ */
35
+ products?: {
36
+ sku?: string
37
+ name?: string
38
+ quantity?: number
39
+ price: number
40
+ description?: string
41
+ category?: string
42
+ url?: string
43
+ image_url?: string
44
+ }[]
45
+ /**
46
+ * The user's customer ID.
47
+ */
48
+ customerId?: string
49
+ /**
50
+ * The user's anonymous ID.
51
+ */
52
+ anonymousId?: string
53
+ /**
54
+ * The user's email address.
55
+ */
56
+ email?: string
57
+ /**
58
+ * Flag to indicate whether the user is a new customer.
59
+ */
60
+ isNewCustomer?: boolean
61
+ /**
62
+ * The status of the user in your loyalty program. Valid values are "in", "out", or "blocked".
63
+ */
64
+ loyaltyStatus?: string
65
+ /**
66
+ * The user's given name.
67
+ */
68
+ firstName?: string
69
+ /**
70
+ * The user's surname.
71
+ */
72
+ lastName?: string
73
+ /**
74
+ * The user's full name.
75
+ */
76
+ name?: string
77
+ /**
78
+ * The user's age.
79
+ */
80
+ age?: number
81
+ /**
82
+ * The user's birthday in the format "YYYY-MM-DD", or "0000-MM-DD" to omit the year.
83
+ */
84
+ birthday?: string
85
+ /**
86
+ * Custom attributes to send to Friendbuy. You should pass an object whose keys are the names of the custom attributes and whose values are strings. Non-string-valued attributes will be dropped.
87
+ */
88
+ friendbuyAttributes?: {
89
+ [k: string]: unknown
90
+ }
91
+ }
@@ -0,0 +1,85 @@
1
+ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
2
+
3
+ import type { FriendbuyAPI } from '../types'
4
+ import type { Settings } from '../generated-types'
5
+ import type { Payload } from './generated-types'
6
+ import type { AnalyticsPayload, ConvertFun, EventMap } from '@segment/actions-shared'
7
+
8
+ import { COPY, ROOT, mapEvent } from '@segment/actions-shared'
9
+ import { trackPurchaseFields } from '@segment/actions-shared'
10
+ import {
11
+ addName,
12
+ enjoinInteger,
13
+ enjoinNumber,
14
+ enjoinString,
15
+ parseDate,
16
+ removeCustomerIfNoId
17
+ } from '@segment/actions-shared'
18
+
19
+ export const browserTrackPurchaseFields = trackPurchaseFields({})
20
+
21
+ // see https://segment.com/docs/config-api/fql/
22
+ export const trackPurchaseDefaultSubscription = 'event = "Order Completed"'
23
+
24
+ const trackPurchasePub: EventMap = {
25
+ fields: {
26
+ orderId: { name: 'id', convert: enjoinString as ConvertFun },
27
+ amount: { convert: enjoinNumber as ConvertFun },
28
+ currency: COPY,
29
+ coupon: { name: 'couponCode' },
30
+ attributionId: COPY,
31
+ referralCode: COPY,
32
+ giftCardCodes: {
33
+ type: 'array'
34
+ },
35
+
36
+ products: {
37
+ type: 'array',
38
+ defaultObject: { sku: 'unknown', name: 'unknown', quantity: 1 },
39
+ fields: {
40
+ sku: { convert: enjoinString as ConvertFun },
41
+ name: COPY,
42
+ quantity: { convert: enjoinInteger as ConvertFun },
43
+ price: { convert: enjoinNumber as ConvertFun },
44
+ description: COPY,
45
+ category: COPY,
46
+ url: COPY,
47
+ image_url: { name: 'imageUrl' }
48
+ }
49
+ },
50
+
51
+ // CUSTOMER FIELDS
52
+ customerId: { name: ['customer', 'id'], convert: enjoinString as ConvertFun },
53
+ anonymousId: { name: ['customer', 'anonymousId'] },
54
+ email: { name: ['customer', 'email'] },
55
+ isNewCustomer: { name: ['customer', 'isNewCustomer'] },
56
+ loyaltyStatus: { name: ['customer', 'loyaltyStatus'] },
57
+ firstName: { name: ['customer', 'firstName'] },
58
+ lastName: { name: ['customer', 'lastName'] },
59
+ name: { name: ['customer', 'name'] },
60
+ age: { name: ['customer', 'age'], convert: enjoinInteger as ConvertFun },
61
+ // fbt-merchant-api complains about birthday being an object but passes it anyway.
62
+ birthday: { name: ['customer', 'birthday'], convert: parseDate as ConvertFun }
63
+ },
64
+ unmappedFieldObject: ROOT,
65
+ // Documentation of 2021-12-03 claims that both customer.id and
66
+ // customer.email are required, but experimentation shows only customer.id
67
+ // is required by pub trackPurchase.
68
+ finalize: removeCustomerIfNoId
69
+ }
70
+
71
+ const action: BrowserActionDefinition<Settings, FriendbuyAPI, Payload> = {
72
+ title: 'Track Purchase',
73
+ description: 'Record when a customer makes a purchase.',
74
+ defaultSubscription: trackPurchaseDefaultSubscription,
75
+ platform: 'web',
76
+ fields: browserTrackPurchaseFields,
77
+
78
+ perform: (friendbuyAPI, { payload }) => {
79
+ addName(payload)
80
+ const friendbuyPayload = mapEvent(trackPurchasePub, payload as unknown as AnalyticsPayload)
81
+ friendbuyAPI.push(['track', 'purchase', friendbuyPayload, true])
82
+ }
83
+ }
84
+
85
+ export default action
@@ -0,0 +1,92 @@
1
+ import { Analytics, Context } from '@segment/analytics-next'
2
+ import friendbuyDestination from '../../index'
3
+ import trackSignUpObject, { browserTrackSignUpFields, trackSignUpDefaultSubscription } from '../index'
4
+
5
+ import { loadScript } from '@segment/browser-destination-runtime/load-script'
6
+ jest.mock('@segment/browser-destination-runtime/load-script')
7
+ beforeEach(async () => {
8
+ // Prevent friendbuy.js and campaigns.js from being loaded.
9
+ ;(loadScript as jest.Mock).mockResolvedValue(true)
10
+ })
11
+
12
+ describe('Friendbuy.trackSignUp', () => {
13
+ const subscriptions = [
14
+ {
15
+ partnerAction: 'trackSignUp',
16
+ name: trackSignUpObject.title,
17
+ enabled: true,
18
+ subscribe: trackSignUpDefaultSubscription,
19
+ mapping: Object.fromEntries(
20
+ Object.entries(browserTrackSignUpFields).map(([name, value]) => [name, value.default])
21
+ )
22
+ }
23
+ ]
24
+
25
+ test('all fields', async () => {
26
+ const merchantId = '1993d0f1-8206-4336-8c88-64e170f2419e'
27
+ const userId = 'john-doe-12345'
28
+ const anonymousId = '6afc2ff2-cf54-414f-9a99-b3adb054ae31'
29
+ const email = 'john.doe@example.com'
30
+ const isNewCustomer = false
31
+ const loyaltyStatus = 'in'
32
+ const firstName = 'John'
33
+ const lastName = 'Doe'
34
+ const name = `${firstName} ${lastName}`
35
+ const age = 42
36
+ const birthday = '0000-12-31'
37
+ const friendbuyAttributes = { custom1: 'custom1', custom2: 'custom2' }
38
+
39
+ const [trackSignUp] = await friendbuyDestination({
40
+ merchantId,
41
+ subscriptions
42
+ })
43
+ // console.log('trackSignUp', JSON.stringify(trackSignUp, null, 2), trackSignUp)
44
+ expect(trackSignUp).toBeDefined()
45
+
46
+ await trackSignUp.load(Context.system(), {} as Analytics)
47
+
48
+ // console.log(window.friendbuyAPI)
49
+ jest.spyOn(window.friendbuyAPI as any, 'push')
50
+
51
+ const context = new Context({
52
+ type: 'track',
53
+ event: 'Signed Up',
54
+ userId,
55
+ anonymousId,
56
+ properties: {
57
+ email,
58
+ isNewCustomer,
59
+ loyaltyStatus,
60
+ firstName,
61
+ lastName,
62
+ name,
63
+ age,
64
+ birthday,
65
+ friendbuyAttributes
66
+ }
67
+ })
68
+ // console.log('context', JSON.stringify(context, null, 2))
69
+
70
+ trackSignUp.track?.(context)
71
+
72
+ // console.log('trackSignUp request', JSON.stringify(window.friendbuyAPI.push.mock.calls[0], null, 2))
73
+ expect(window.friendbuyAPI?.push).toHaveBeenCalledWith([
74
+ 'track',
75
+ 'sign_up',
76
+ {
77
+ id: userId,
78
+ email,
79
+ isNewCustomer,
80
+ loyaltyStatus,
81
+ firstName,
82
+ lastName,
83
+ name,
84
+ age,
85
+ anonymousId,
86
+ birthday: { month: 12, day: 31 },
87
+ ...friendbuyAttributes
88
+ },
89
+ true
90
+ ])
91
+ })
92
+ })
@@ -0,0 +1,62 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The user's customer ID.
6
+ */
7
+ customerId: string
8
+ /**
9
+ * The user's anonymous ID.
10
+ */
11
+ anonymousId?: string
12
+ /**
13
+ * The user's email address.
14
+ */
15
+ email: string
16
+ /**
17
+ * Flag to indicate whether the user is a new customer.
18
+ */
19
+ isNewCustomer?: boolean
20
+ /**
21
+ * The status of the user in your loyalty program. Valid values are "in", "out", or "blocked".
22
+ */
23
+ loyaltyStatus?: string
24
+ /**
25
+ * The user's given name.
26
+ */
27
+ firstName?: string
28
+ /**
29
+ * The user's surname.
30
+ */
31
+ lastName?: string
32
+ /**
33
+ * The user's full name.
34
+ */
35
+ name?: string
36
+ /**
37
+ * The user's age.
38
+ */
39
+ age?: number
40
+ /**
41
+ * The user's birthday in the format "YYYY-MM-DD", or "0000-MM-DD" to omit the year.
42
+ */
43
+ birthday?: string
44
+ /**
45
+ * Coupon code that customer supplied when they signed up.
46
+ */
47
+ coupon?: string
48
+ /**
49
+ * Friendbuy attribution ID that associates the customer who is signing up with the advocate who referred them.
50
+ */
51
+ attributionId?: string
52
+ /**
53
+ * Friendbuy referral code that associates the customer who is signing up with the advocate who referred them.
54
+ */
55
+ referralCode?: string
56
+ /**
57
+ * Custom attributes to send to Friendbuy. You should pass an object whose keys are the names of the custom attributes and whose values are strings. Non-string-valued attributes will be dropped.
58
+ */
59
+ friendbuyAttributes?: {
60
+ [k: string]: unknown
61
+ }
62
+ }