@splendidlabz/tracking 0.2.10 → 0.2.11

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/lib/fb/node.js CHANGED
@@ -18,9 +18,27 @@ export default function FB({ pixelID, testEventCode, accessToken, framework }) {
18
18
  // When used as API call — frontend would have done all the work
19
19
  // So we can just send the body to the CAPI request
20
20
  async sendEvent(props = {}, options = {}) {
21
- const { pixel_id, test_event_code, ...rest } = props
21
+ const { pixel_id, test_event_code, user_data, ...rest } = props
22
+ const { context } = options
23
+
24
+ const {
25
+ p: { fbc, fbp },
26
+ o: userProps,
27
+ } = splitObject(user_data, ['fbc', 'fbp'])
28
+
29
+ const data = {
30
+ ...rest,
31
+ user_data: await normalizeUserData({
32
+ context,
33
+ framework,
34
+ userProps,
35
+ fbc,
36
+ fbp,
37
+ }),
38
+ }
39
+
22
40
  const body = {
23
- data: [omitEmpty(rest)],
41
+ data: [omitEmpty(data)],
24
42
  test_event_code: test_event_code || testEventCode,
25
43
  }
26
44
  return this.sendCAPIRequest(body, { ...options, pixel_id })
@@ -44,8 +62,11 @@ export default function FB({ pixelID, testEventCode, accessToken, framework }) {
44
62
  )
45
63
  }
46
64
 
47
- if (framework !== 'astro') {
48
- throw new Error('Currently we only support the Astro framework')
65
+ const supportedFrameworks = ['astro', 'express', 'fastify', 'koa']
66
+ if (!supportedFrameworks.includes(framework)) {
67
+ throw new Error(
68
+ `Unsupported framework: ${framework}. Supported frameworks: ${supportedFrameworks.join(', ')}`,
69
+ )
49
70
  }
50
71
 
51
72
  const normalizedUserData = await normalizeUserData({
@@ -97,6 +118,9 @@ export default function FB({ pixelID, testEventCode, accessToken, framework }) {
97
118
  }
98
119
  }
99
120
 
121
+ // ========================
122
+ // Helper Functions
123
+ // ========================
100
124
  function getEventParamDefaults(framework, context) {
101
125
  return {
102
126
  event_id: randomString(10),
@@ -108,11 +132,40 @@ function getEventParamDefaults(framework, context) {
108
132
 
109
133
  function getUrl(framework, context) {
110
134
  if (framework === 'astro') return context.url.href
135
+ if (framework === 'express') {
136
+ return context.protocol + '://' + context.get('host') + context.originalUrl
137
+ }
138
+ if (framework === 'fastify') {
139
+ return context.protocol + '://' + context.hostname + context.url
140
+ }
141
+ if (framework === 'koa') {
142
+ return context.protocol + '://' + context.host + context.url
143
+ }
111
144
  }
112
145
 
113
- async function normalizeUserData({ context, framework, userProps, fbc, fbp }) {
114
- let ip, userAgent
146
+ function getIP(framework, context) {
147
+ if (framework === 'astro') return context.clientAddress
148
+ if (framework === 'express')
149
+ return context.ip || context.connection?.remoteAddress
150
+ if (framework === 'fastify') return context.ip
151
+ if (framework === 'koa') return context.ip
152
+ }
153
+
154
+ function getUserAgent(framework, context) {
155
+ if (framework === 'astro') return context.request.headers.get('user-agent')
156
+ if (framework === 'express') return context.get('user-agent')
157
+ if (framework === 'fastify') return context.headers['user-agent']
158
+ if (framework === 'koa') return context.get('user-agent')
159
+ }
115
160
 
161
+ function getCookie(framework, context, name) {
162
+ if (framework === 'astro') return context.cookies.get(name)?.value
163
+ if (framework === 'express') return context.cookies?.[name]
164
+ if (framework === 'fastify') return context.cookies?.[name]
165
+ if (framework === 'koa') return context.cookies?.get(name)
166
+ }
167
+
168
+ async function normalizeUserData({ context, framework, userProps, fbc, fbp }) {
116
169
  const { p: userPropsToHash, o: otherUserProps } = splitObject(
117
170
  userProps,
118
171
  USER_PROPERTIES_TO_HASH,
@@ -120,13 +173,11 @@ async function normalizeUserData({ context, framework, userProps, fbc, fbp }) {
120
173
 
121
174
  const hashedProps = await formatUserData(userPropsToHash, sha256Hash)
122
175
 
123
- // Get other client data
124
- if (framework === 'astro') {
125
- ip = context.clientAddress
126
- userAgent = context.request.headers.get('user-agent')
127
- if (!fbp) fbp = context.cookies.get('_fbp')?.value
128
- if (!fbc) fbc = context.cookies.get('_fbc')?.value
129
- }
176
+ // Get client data from context
177
+ const ip = getIP(framework, context)
178
+ const userAgent = getUserAgent(framework, context)
179
+ if (!fbp) fbp = getCookie(framework, context, '_fbp')
180
+ if (!fbc) fbc = getCookie(framework, context, '_fbc')
130
181
 
131
182
  const userData = omitEmpty({
132
183
  ...hashedProps,
package/lib/fb/readme.md CHANGED
@@ -1,42 +1,43 @@
1
- Shared config
1
+ Designed to provide a unified interface for Facebook Pixel and Conversion API.
2
+
3
+ ## Setup
4
+
5
+ Config
2
6
 
3
7
  ```js
4
8
  export default {
5
9
  fb: {
6
10
  pixelID: 'PIXEL ID',
7
11
  testEventCode: 'TEST EVENT CODE',
12
+ capiEndpoint: '/api/tracking/facebook/',
13
+ accessToken: import.meta.env.FB_CAPI_TOKEN,
14
+ framework: 'astro', // or 'express', 'fastify', 'koa'
8
15
  },
9
16
  }
10
17
  ```
11
18
 
12
- Setup
19
+ Browser
13
20
 
14
21
  ```js
15
22
  import FB from '@splendidlabz/tracking/fb/web'
16
- import config from './shared'
17
-
18
- export const fb = FB({
19
- ...config.fb,
20
- capiEndpoint: '/api/tracking/facebook/',
21
- })
23
+ import config from './config'
24
+ export const fb = FB(config.fb)
22
25
  ```
23
26
 
24
27
  Server
25
28
 
26
29
  ```js
27
30
  import FB from '@splendidlabz/tracking/fb/node'
28
- import config from './shared'
29
-
30
- export const fb = FB({
31
- ...config.fb,
32
- framework: 'astro',
33
- accessToken: import.meta.env.FB_CAPI_TOKEN,
34
- })
31
+ import config from './config'
32
+ export const fb = FB(config.fb)
35
33
  ```
36
34
 
37
- Ads server for dedupe events
35
+ Include API to dedupe events through CAPI
36
+
37
+ **Astro:**
38
38
 
39
39
  ```js
40
+ // /api/tracking/facebook/
40
41
  import { fb } from '@/services/tracking/node'
41
42
  import { JSONResponse, parseData } from '@splendidlabz/astro/server'
42
43
  export const prerender = false
@@ -49,6 +50,143 @@ export async function POST(context) {
49
50
  }
50
51
  ```
51
52
 
52
- Other stuff necessary.... well... check Facebook docs ba.
53
- Perhaps should include a standard event list , or link to the docs.
54
- Should also notify that `PageView` is going to be tracked as a custom event.
53
+ **Express:**
54
+
55
+ ```js
56
+ // Requires cookie-parser middleware for cookie support
57
+ import express from 'express'
58
+ import cookieParser from 'cookie-parser'
59
+ import { fb } from './services/tracking/node.js'
60
+
61
+ const app = express()
62
+ app.use(express.json())
63
+ app.use(cookieParser())
64
+
65
+ app.post('/api/tracking/facebook', async (req, res) => {
66
+ const { response, error } = await fb.sendEvent(req.body)
67
+ if (response) return res.status(200).json(response.body)
68
+ if (error) return res.status(500).json(error)
69
+ })
70
+ ```
71
+
72
+ **Fastify:**
73
+
74
+ ```js
75
+ // Requires @fastify/cookie plugin for cookie support
76
+ import Fastify from 'fastify'
77
+ import fastifyCookie from '@fastify/cookie'
78
+ import { fb } from './services/tracking/node.js'
79
+
80
+ const fastify = Fastify()
81
+ await fastify.register(fastifyCookie)
82
+
83
+ fastify.post('/api/tracking/facebook', async (request, reply) => {
84
+ const { response, error } = await fb.sendEvent(request.body)
85
+ if (response) return reply.status(200).send(response.body)
86
+ if (error) return reply.status(500).send(error)
87
+ })
88
+ ```
89
+
90
+ **Koa:**
91
+
92
+ ```js
93
+ // Requires koa-bodyparser and koa-cookie middleware
94
+ import Koa from 'koa'
95
+ import bodyParser from 'koa-bodyparser'
96
+ import cookie from 'koa-cookie'
97
+ import { fb } from './services/tracking/node.js'
98
+
99
+ const app = new Koa()
100
+ app.use(bodyParser())
101
+ app.use(cookie())
102
+
103
+ app.use(async (ctx) => {
104
+ if (ctx.path === '/api/tracking/facebook' && ctx.method === 'POST') {
105
+ const { response, error } = await fb.sendEvent(ctx.request.body)
106
+ if (response) {
107
+ ctx.status = 200
108
+ ctx.body = response.body
109
+ }
110
+ if (error) {
111
+ ctx.status = 500
112
+ ctx.body = error
113
+ }
114
+ }
115
+ })
116
+ ```
117
+
118
+ ## Usage
119
+
120
+ Browser:
121
+
122
+ ```js
123
+ fb.track('event_name', { ...props })
124
+ ```
125
+
126
+ Node (requires passing the request context):
127
+
128
+ **Astro:**
129
+
130
+ ```js
131
+ fb.track('event_name', { context, ...props })
132
+ ```
133
+
134
+ **Express:**
135
+
136
+ ```js
137
+ app.post('/some-route', async (req, res) => {
138
+ await fb.track('event_name', { context: req, ...props })
139
+ res.json({ success: true })
140
+ })
141
+ ```
142
+
143
+ **Fastify:**
144
+
145
+ ```js
146
+ fastify.post('/some-route', async (request, reply) => {
147
+ await fb.track('event_name', { context: request, ...props })
148
+ reply.send({ success: true })
149
+ })
150
+ ```
151
+
152
+ **Koa:**
153
+
154
+ ```js
155
+ app.use(async (ctx) => {
156
+ if (ctx.path === '/some-route' && ctx.method === 'POST') {
157
+ await fb.track('event_name', { context: ctx, ...props })
158
+ ctx.body = { success: true }
159
+ }
160
+ })
161
+ ```
162
+
163
+ ## Event Reference
164
+
165
+ - [Standard Event Reference](https://developers.facebook.com/docs/meta-pixel/reference#standard-events)
166
+ - [Advanced Matching Reference](https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/)
167
+ - [Conversion API Params Reference](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters)
168
+
169
+ ## Testing Your Implementation
170
+
171
+ Go to `Events Manager > Datasets > Test Events` and copy your event tracking code. Add it to your config.
172
+
173
+ **IMPORTANT**: Remove `testEventCode` after your tests! You don't want this in your production code.
174
+
175
+ ```js
176
+ export default {
177
+ fb: {
178
+ pixelID: 'PIXEL ID',
179
+ testEventCode: 'TEST EVENT CODE',
180
+ // ...
181
+ },
182
+ }
183
+ ```
184
+
185
+ Under the same `Test Events` tab, enter a website to test with.
186
+
187
+ - Note that localhost cannot be used for browser-testing — FB requires a valid domain
188
+ - To test if server & browser events are deduplicated, use a local tunnel like ngrok.
189
+ - Make sure your website — and your tunnel — is listed under your Pixel's allow list under
190
+ `Dataset > Settings > Allow list`. Feel free to remove the tunnel from the allow list after testing.
191
+
192
+ - Context is to automatically get Client IP Address and User Agent.
package/lib/fb/web.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { notEmpty, splitObject } from '@splendidlabz/utils'
2
2
  import { getCookie, randomString, sha256Hash } from '@splendidlabz/utils/dom'
3
+ import zlFetch from 'zl-fetch'
3
4
  import {
4
5
  STANDARD_EVENTS,
5
6
  USER_PROPERTIES,
6
7
  USER_PROPERTIES_TO_HASH,
7
8
  } from './consts.js'
8
-
9
- import zlFetch from 'zl-fetch'
10
9
  import { formatUserData } from './utils.js'
11
10
 
12
11
  export default function FB({ pixelID, testEventCode, capiEndpoint }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splendidlabz/tracking",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Simple Tracking Helper",
5
5
  "type": "module",
6
6
  "files": [