@splendidlabz/tracking 0.1.20

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,35 @@
1
+ import { sessionStore } from '@splendidlabz/utils/dom'
2
+ import zlFetch from 'zl-fetch'
3
+
4
+ // Link this to Convertkit... if there is a convertkit id in the URL, identify the user with that id.
5
+ export function getSubscriberID() {
6
+ // Get from Session Store
7
+ let ckid = sessionStore.get('ckid')
8
+ if (ckid) return ckid
9
+
10
+ // Get from URL
11
+ const url = new URL(window.location.href)
12
+ ckid = url.searchParams.get('ck_subscriber_id')
13
+
14
+ if (ckid) {
15
+ // Cleanup URL
16
+ url.searchParams.delete('ck_subscriber_id')
17
+ window.history.replaceState({}, '', url.href)
18
+ sessionStore.set('ckid', ckid)
19
+ return ckid
20
+ }
21
+ }
22
+
23
+ // Probably needs to have an initialize endpoint somewhere so can use easily... maybe include a config file or something and inject that in?
24
+ // Problem now is whether I want to sell this part as well... if not then it becomes lehcheh.
25
+ export async function getSubscriberInfo(id, { apiRoot }) {
26
+ // Get info from session store if available
27
+ let ckinfo = sessionStore.get('ckinfo')
28
+ if (ckinfo) return ckinfo
29
+
30
+ // Get from server otherwise
31
+ const response = await zlFetch(`${apiRoot}/ck/subscribers/${id}`)
32
+ ckinfo = response.body
33
+ sessionStore.set('ckinfo', ckinfo)
34
+ return ckinfo
35
+ }
@@ -0,0 +1,24 @@
1
+ import createError from 'http-errors'
2
+ import zlFetch from 'zl-fetch'
3
+
4
+ const SECRET = process.env.CONVERTKIT_API_SECRET
5
+
6
+ export async function getSubscriber(id) {
7
+ const endpoint = `https://api.convertkit.com/v3/subscribers/${id}`
8
+
9
+ if (!accessToken) {
10
+ throw createError(
11
+ 500,
12
+ 'Convertkit API Secret is not provided in the environment variables.'
13
+ )
14
+ return
15
+ }
16
+
17
+ const response = await zlFetch(endpoint, {
18
+ query: {
19
+ api_secret: SECRET,
20
+ },
21
+ })
22
+
23
+ return response.body
24
+ }
@@ -0,0 +1,184 @@
1
+ import { getCookie, randomString, sha256Hash } from '@splendidlabz/utils/dom'
2
+ import { isEmpty, notEmpty, splitObject } from '@splendidlabz/utils'
3
+
4
+ import { formatUserData } from './utils.js'
5
+ import zlFetch from 'zl-fetch'
6
+
7
+ // TODO: Figure a way to put the Pixel Code into the DOM... if possible!
8
+ export default function FB({ pixelID, testEventCode, capiEndpoint }) {
9
+ const _fb = {
10
+ options: { pixelID, testEventCode, capiEndpoint },
11
+
12
+ // Initializes Facebook
13
+ init() {
14
+ !(function (f, b, e, v, n, t, s) {
15
+ if (f.fbq) return
16
+ n = f.fbq = function () {
17
+ n.callMethod
18
+ ? n.callMethod.apply(n, arguments)
19
+ : n.queue.push(arguments)
20
+ }
21
+ if (!f._fbq) f._fbq = n
22
+ n.push = n
23
+ n.loaded = !0
24
+ n.version = '2.0'
25
+ n.queue = []
26
+ t = b.createElement(e)
27
+ t.async = !0
28
+ t.src = v
29
+ s = b.getElementsByTagName(e)[0]
30
+ s.parentNode.insertBefore(t, s)
31
+ })(
32
+ window,
33
+ document,
34
+ 'script',
35
+ 'https://connect.facebook.net/en_US/fbevents.js',
36
+ )
37
+ fbq('init', pixelID)
38
+ },
39
+
40
+ async sendEvent(eventName, props = {}) {
41
+ const eventData = await _fb.createFBEvent(eventName, props)
42
+
43
+ // Question whether we want to verbose log this thing in the future so users can know whether these events are sent.
44
+ _fb.sendPixelEvent(eventData)
45
+ return _fb.sendCAPIEvent(eventData)
46
+ },
47
+
48
+ async createFBEvent(eventName, props = {}) {
49
+ const { eventURL, actionSource, ...rest } = props
50
+
51
+ const { picked: userProperties, omitted: customData } = splitObject(
52
+ rest,
53
+ [...USER_PROPERTIES_TO_HASH, ...USER_PROPERTIES_NOT_TO_HASH],
54
+ )
55
+
56
+ const { picked: userPropsToHash, omitted: otherUserProps } = splitObject(
57
+ userProperties,
58
+ USER_PROPERTIES_TO_HASH,
59
+ )
60
+
61
+ const userData = {
62
+ ...(await formatUserData(userPropsToHash)),
63
+ ...otherUserProps,
64
+ }
65
+
66
+ const ret = {
67
+ // Data for both Pixel and CAPI
68
+ pixelID,
69
+ eventID: await randomString(),
70
+ eventName: eventName,
71
+ userData,
72
+ customData,
73
+
74
+ // Data for CAPI only
75
+ fbp: getCookie('_fbp'),
76
+ fbc: getCookie('_fbc'),
77
+ eventTime: Math.floor(new Date() / 1000),
78
+ eventURL: eventURL || window.location.href,
79
+ actionSource: actionSource || 'website',
80
+ }
81
+ if (testEventCode) ret.testEventCode = testEventCode
82
+
83
+ return ret
84
+ },
85
+
86
+ // Facebook Pixel Code
87
+ // Pixel Standard Reference
88
+ // https://developers.facebook.com/docs/meta-pixel/reference#standard-events
89
+ sendPixelEvent(data) {
90
+ const {
91
+ eventID,
92
+ eventName,
93
+ userData = {},
94
+ customData = {},
95
+ pixelID,
96
+ } = data
97
+ const props = { ...userData, ...customData }
98
+
99
+ let action = 'track'
100
+ let evtName = eventName
101
+ if (evtName === 'init') action = 'init'
102
+
103
+ // When we use fbq init, we have to add the pixelID instead of the event name as the 2nd parameter.
104
+ if (action === 'init') evtName = pixelID
105
+
106
+ // If the event is a custom event, we need to use `trackCustom` instead of `track`.
107
+ if (action !== 'init' && !standardEvents.includes(evtName))
108
+ action = 'trackCustom'
109
+
110
+ fbq(action, evtName, props, { eventID })
111
+ },
112
+
113
+ // CAPI Parameters: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
114
+ sendCAPIEvent(data) {
115
+ return zlFetch.post(capiEndpoint, { body: data })
116
+ },
117
+ }
118
+
119
+ return _fb
120
+ }
121
+
122
+ const standardEvents = [
123
+ 'AddPaymentInfo',
124
+ 'AddToCart',
125
+ 'AddToWishlist',
126
+ 'CompleteRegistration',
127
+ 'Contact',
128
+ 'CustomizeProduct',
129
+ 'Donate',
130
+ 'FindLocation',
131
+ 'InitiateCheckout',
132
+ 'Lead',
133
+ 'Purchase',
134
+ 'Schedule',
135
+ 'Search',
136
+ 'StartTrial',
137
+ 'SubmitApplication',
138
+ 'Subscribe',
139
+ 'ViewContent',
140
+ ]
141
+
142
+ // https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
143
+ const USER_PROPERTIES_TO_HASH = [
144
+ 'email',
145
+ 'phone',
146
+ 'firstName',
147
+ 'lastName',
148
+ 'birthdate',
149
+ 'gender',
150
+ 'city',
151
+ 'state',
152
+ 'zip',
153
+ 'country',
154
+ 'externalID',
155
+ ]
156
+
157
+ // Other optional FB Stuff — doesn't seem very useful
158
+ const USER_PROPERTIES_NOT_TO_HASH = [
159
+ 'subscription_id',
160
+ 'fb_login_id',
161
+ 'lead_id',
162
+ 'anon_id',
163
+ 'madid',
164
+ 'page_id',
165
+ 'page_scoped_user_id',
166
+ 'ctwa_clid',
167
+ 'ig_account_id',
168
+ 'ig_sid',
169
+ ]
170
+
171
+ const CUSTOM_PROPERTIES = [
172
+ 'content_category',
173
+ 'content_ids',
174
+ 'content_name',
175
+ 'content_type',
176
+ 'contents',
177
+ 'currency',
178
+ 'delivery_category',
179
+ 'num_items',
180
+ 'predicted_ltv',
181
+ 'search_string',
182
+ 'status',
183
+ 'value',
184
+ ]
@@ -0,0 +1,127 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import FB from './browser.js'
5
+ import { sha256Hash } from '@splendidlabz/utils/dom'
6
+
7
+ describe('Facebook Event Props', _ => {
8
+ it('Basic Event Props', async _ => {
9
+ const fb = new FB({
10
+ pixelID: '123456789',
11
+ testEventCode: 'TEST123',
12
+ capiEndpoint: 'https://somewhere.com',
13
+ })
14
+
15
+ const eventData = await fb.createFBEvent('PageView', {})
16
+
17
+ expect(eventData).toHaveProperty('pixelID')
18
+ expect(eventData).toHaveProperty('eventID')
19
+ expect(eventData).toHaveProperty('eventName')
20
+ expect(eventData).toHaveProperty('eventURL')
21
+ expect(eventData).toHaveProperty('eventTime')
22
+ expect(eventData).toHaveProperty('actionSource')
23
+ expect(eventData).toHaveProperty('fbp')
24
+ expect(eventData).toHaveProperty('fbc')
25
+ expect(eventData).toHaveProperty('userData')
26
+ expect(eventData).toHaveProperty('customData')
27
+ })
28
+
29
+ it('Correctly splits userData and customData', async _ => {
30
+ const fb = new FB({
31
+ pixelID: '123456789',
32
+ testEventCode: 'TEST123',
33
+ capiEndpoint: 'https://somewhere.com',
34
+ })
35
+
36
+ const eventData = await fb.createFBEvent('Lead', {
37
+ email: 'abc@test.com',
38
+ firstName: 'ABC',
39
+ contents: [
40
+ { id: '22', quantity: 1 },
41
+ { id: '23', quantity: 1 },
42
+ ],
43
+ })
44
+
45
+ const { userData, customData } = eventData
46
+
47
+ expect(userData).toHaveProperty('em')
48
+ expect(userData).toHaveProperty('fn')
49
+ expect(customData).toEqual({
50
+ contents: [
51
+ { id: '22', quantity: 1 },
52
+ { id: '23', quantity: 1 },
53
+ ],
54
+ })
55
+ })
56
+
57
+ it('Hashing User Properties', async _ => {
58
+ const fb = new FB({
59
+ pixelID: '123456789',
60
+ testEventCode: 'TEST123',
61
+ capiEndpoint: 'https://somewhere.com',
62
+ })
63
+
64
+ const eventData = await fb.createFBEvent('Lead', {
65
+ email: 'zellwk@gmail.com',
66
+ firstName: 'Zell',
67
+ lastName: 'Liew',
68
+ birthdate: '19900101',
69
+ gender: 'm',
70
+ city: 'Singapore',
71
+ country: 'Singapore',
72
+ state: 'Singapore',
73
+ zip: '12345',
74
+ })
75
+
76
+ const { userData } = eventData
77
+
78
+ expect(userData).toEqual({
79
+ em: '5fc6517c0a210f60c685fa4e21c1c4a8a9862c55ed37f2db25cb4ae893f8e9a9',
80
+ fn: '07bea691e1173e5366d4ddf2c13103802b3f16441c81d6a19037a6d88118b46c',
81
+ ln: '555ba31c9795b7e549c88014751487db1310a2daaabd891555c3c34156e9e531',
82
+ db: 'd6165fc8037886424321260c0f9c2a76f7d8d545a02f95c70da9952a13299f33',
83
+ ge: '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a',
84
+ ct: '51e2a46721d104d9148d85b617833e7745fdbd6795cb0b502a5b6ea31d33378e',
85
+ country:
86
+ '88195283220112932b02b8aa03dc289106f478e998cadaeeea2e181f8aa1a01d',
87
+ st: '51e2a46721d104d9148d85b617833e7745fdbd6795cb0b502a5b6ea31d33378e',
88
+ zp: '5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5',
89
+ })
90
+ })
91
+
92
+ it('Not Hashing certain user properties', async _ => {
93
+ const fb = new FB({
94
+ pixelID: '123456789',
95
+ testEventCode: 'TEST123',
96
+ capiEndpoint: 'https://somewhere.com',
97
+ })
98
+
99
+ const eventData = await fb.createFBEvent('Lead', {
100
+ subscription_id: '123456789',
101
+ fb_login_id: '123456789',
102
+ lead_id: '123456789',
103
+ anon_id: '123456789',
104
+ madid: '123456789',
105
+ page_id: '123456789',
106
+ page_scoped_user_id: '123456789',
107
+ ctwa_clid: '123456789',
108
+ ig_account_id: '123456789',
109
+ ig_sid: '123456789',
110
+ })
111
+
112
+ const { userData } = eventData
113
+
114
+ expect(userData).toEqual({
115
+ subscription_id: '123456789',
116
+ fb_login_id: '123456789',
117
+ lead_id: '123456789',
118
+ anon_id: '123456789',
119
+ madid: '123456789',
120
+ page_id: '123456789',
121
+ page_scoped_user_id: '123456789',
122
+ ctwa_clid: '123456789',
123
+ ig_account_id: '123456789',
124
+ ig_sid: '123456789',
125
+ })
126
+ })
127
+ })
@@ -0,0 +1,73 @@
1
+ import createError from 'http-errors'
2
+ import { formatUserData } from './utils.js'
3
+ import zlFetch from 'zl-fetch'
4
+
5
+ const accessToken = process.env.FB_ACCESS_TOKEN
6
+
7
+ export async function sendConversionAPIRequest(req) {
8
+ const {
9
+ pixelID,
10
+ eventID,
11
+ eventName,
12
+ actionSource,
13
+ eventURL,
14
+ eventTime,
15
+ userData,
16
+ customData,
17
+ fbp,
18
+ fbc,
19
+ testEventCode,
20
+ ...rest
21
+ } = req.body
22
+
23
+ if (!accessToken) {
24
+ throw createError(
25
+ 500,
26
+ 'Facebook Access Token is not provided in the environment variables.'
27
+ )
28
+ return
29
+ }
30
+
31
+ // Don't send init event for CAPI
32
+ if (eventName === 'init') return
33
+
34
+ const outgoingData = [
35
+ {
36
+ event_id: eventID, // For deduplication
37
+ event_name: eventName,
38
+ action_source: actionSource,
39
+ event_source_url: eventURL,
40
+ event_time: eventTime,
41
+ user_data: {
42
+ ...userData,
43
+ client_ip_address: req.clientIp, // Checks if this works... if not, use ipify from the browser.
44
+ client_user_agent: req.headers['user-agent'],
45
+ fbc: fbc,
46
+ fbp: fbp,
47
+ },
48
+ custom_data: customData,
49
+
50
+ // Optional data processing stuff
51
+ opt_out: data.opt_out || false, //
52
+ data_processing_options: [],
53
+ data_processing_options_country: [],
54
+ data_processing_options_state: [],
55
+ },
56
+ ]
57
+
58
+ // Format CAPI Body
59
+ const body = {
60
+ data: outgoingData,
61
+ access_token: accessToken,
62
+ }
63
+
64
+ if (testEventCode) body.test_event_code = testEventCode
65
+
66
+ // Send the CAPI request
67
+ // Maybe need to update the version here somehow
68
+ return zlFetch('https://graph.facebook.com/v19.0/' + pixelID + '/events', {
69
+ method: 'post',
70
+ auth: accessToken,
71
+ body,
72
+ })
73
+ }