@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.
- package/README.md +5 -0
- package/dist/fb.js +949 -0
- package/lib/convertkit/client.js +35 -0
- package/lib/convertkit/server.js +24 -0
- package/lib/fb/browser.js +184 -0
- package/lib/fb/browser.test.js +127 -0
- package/lib/fb/server.js +73 -0
- package/lib/fb/utils.js +372 -0
- package/lib/posthog/CrossDomainTracking.svelte +10 -0
- package/lib/posthog/cross-domain.js +50 -0
- package/lib/posthog/index.js +1 -0
- package/package.json +38 -0
|
@@ -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
|
+
})
|
package/lib/fb/server.js
ADDED
|
@@ -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
|
+
}
|