@segment/analytics-browser-actions-facebook-conversions-api-web 1.11.1-staging-49cc16573.0 → 1.12.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.
- package/dist/cjs/send/functions.d.ts +5 -2
- package/dist/cjs/send/functions.js +56 -20
- package/dist/cjs/send/functions.js.map +1 -1
- package/dist/cjs/types.d.ts +1 -0
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/send/functions.d.ts +5 -2
- package/dist/esm/send/functions.js +54 -21
- package/dist/esm/send/functions.js.map +1 -1
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/send/__tests__/formatFBEvent.test.ts +282 -0
- package/src/send/__tests__/formatUserData.test.ts +391 -0
- package/src/send/__tests__/functions.test.ts +113 -509
- package/src/send/__tests__/hashing.test.ts +123 -0
- package/src/send/__tests__/init.test.ts +578 -0
- package/src/send/functions.ts +71 -24
- package/src/types.ts +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { sha256Hash } from '../functions'
|
|
2
|
+
|
|
3
|
+
describe('SHA256 Hashing', () => {
|
|
4
|
+
describe('sha256Hash', () => {
|
|
5
|
+
it('should correctly hash an email address', async () => {
|
|
6
|
+
const input = 'test@example.com'
|
|
7
|
+
const expected = '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
|
|
8
|
+
|
|
9
|
+
const result = await sha256Hash(input)
|
|
10
|
+
|
|
11
|
+
expect(result).toBe(expected)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should correctly hash a simple string', async () => {
|
|
15
|
+
const input = 'hello'
|
|
16
|
+
const expected = '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
17
|
+
|
|
18
|
+
const result = await sha256Hash(input)
|
|
19
|
+
|
|
20
|
+
expect(result).toBe(expected)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should correctly hash a phone number (digits only)', async () => {
|
|
24
|
+
const input = '14155551234'
|
|
25
|
+
const expected = 'c6a349dfaaf5c3a368d3135014cc1bc7aebf18f654f313f9c1d0b018a897b209'
|
|
26
|
+
|
|
27
|
+
const result = await sha256Hash(input)
|
|
28
|
+
|
|
29
|
+
expect(result).toBe(expected)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should correctly hash a first name', async () => {
|
|
33
|
+
const input = 'john'
|
|
34
|
+
const expected = '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a'
|
|
35
|
+
|
|
36
|
+
const result = await sha256Hash(input)
|
|
37
|
+
|
|
38
|
+
expect(result).toBe(expected)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should correctly hash a last name', async () => {
|
|
42
|
+
const input = 'doe'
|
|
43
|
+
const expected = '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f'
|
|
44
|
+
|
|
45
|
+
const result = await sha256Hash(input)
|
|
46
|
+
|
|
47
|
+
expect(result).toBe(expected)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should correctly hash a gender value', async () => {
|
|
51
|
+
const input = 'm'
|
|
52
|
+
const expected = '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a'
|
|
53
|
+
|
|
54
|
+
const result = await sha256Hash(input)
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(expected)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should correctly hash a city name (normalized)', async () => {
|
|
60
|
+
const input = 'newyork'
|
|
61
|
+
const expected = '350c754ba4d38897693aa077ef43072a859d23f613443133fecbbd90a3512ca5'
|
|
62
|
+
|
|
63
|
+
const result = await sha256Hash(input)
|
|
64
|
+
|
|
65
|
+
expect(result).toBe(expected)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should correctly hash a date of birth (YYYYMMDD format)', async () => {
|
|
69
|
+
const input = '19900115'
|
|
70
|
+
const expected = '4747c382bedef489a190a6797e6f4451907b86511bdd49cfa8f9d4c1a78d8bac'
|
|
71
|
+
|
|
72
|
+
const result = await sha256Hash(input)
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(expected)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should correctly hash numeric strings', async () => {
|
|
78
|
+
const input = '1234567890'
|
|
79
|
+
const expected = 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646'
|
|
80
|
+
|
|
81
|
+
const result = await sha256Hash(input)
|
|
82
|
+
|
|
83
|
+
expect(result).toBe(expected)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should return a 64-character hex string', async () => {
|
|
87
|
+
const input = 'test'
|
|
88
|
+
|
|
89
|
+
const result = await sha256Hash(input)
|
|
90
|
+
|
|
91
|
+
expect(result).toHaveLength(64)
|
|
92
|
+
expect(result).toMatch(/^[a-f0-9]{64}$/)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should produce different hashes for different inputs', async () => {
|
|
96
|
+
const input1 = 'test1'
|
|
97
|
+
const input2 = 'test2'
|
|
98
|
+
|
|
99
|
+
const result1 = await sha256Hash(input1)
|
|
100
|
+
const result2 = await sha256Hash(input2)
|
|
101
|
+
|
|
102
|
+
expect(result1).not.toBe(result2)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should produce the same hash for the same input', async () => {
|
|
106
|
+
const input = 'consistent'
|
|
107
|
+
|
|
108
|
+
const result1 = await sha256Hash(input)
|
|
109
|
+
const result2 = await sha256Hash(input)
|
|
110
|
+
|
|
111
|
+
expect(result1).toBe(result2)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should handle empty string', async () => {
|
|
115
|
+
const input = ''
|
|
116
|
+
const expected = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
|
117
|
+
|
|
118
|
+
const result = await sha256Hash(input)
|
|
119
|
+
|
|
120
|
+
expect(result).toBe(expected)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { send } from '../functions'
|
|
2
|
+
|
|
3
|
+
describe('Facebook Conversions API Web - Init with User Data', () => {
|
|
4
|
+
let mockFbq
|
|
5
|
+
let mockAnalytics
|
|
6
|
+
let mockClientParamBuilder
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFbq = jest.fn()
|
|
10
|
+
mockAnalytics = {
|
|
11
|
+
storage: {
|
|
12
|
+
get: jest.fn(),
|
|
13
|
+
set: jest.fn()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
mockClientParamBuilder = undefined
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
jest.restoreAllMocks()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const defaultSettings = {
|
|
25
|
+
pixelId: 'test-pixel-123',
|
|
26
|
+
ldu: 'Disabled'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('init call with user data formatting', () => {
|
|
30
|
+
it('should format userData with email', async () => {
|
|
31
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
32
|
+
|
|
33
|
+
const payload = {
|
|
34
|
+
event_config: {
|
|
35
|
+
event_name: 'Purchase',
|
|
36
|
+
show_fields: false
|
|
37
|
+
},
|
|
38
|
+
content_ids: ['product-123'],
|
|
39
|
+
value: 99.99,
|
|
40
|
+
userData: {
|
|
41
|
+
em: 'TEST@EXAMPLE.COM'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
46
|
+
|
|
47
|
+
// Email should be normalized (lowercase, trimmed) and hashed with SHA256
|
|
48
|
+
// 'test@example.com' -> '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
|
|
49
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
50
|
+
em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
|
|
51
|
+
})
|
|
52
|
+
// Should have stored user data
|
|
53
|
+
expect(mockAnalytics.storage.set).toHaveBeenCalledWith(
|
|
54
|
+
'fb_user_data',
|
|
55
|
+
expect.stringContaining('973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b')
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should format userData with phone number', async () => {
|
|
60
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
61
|
+
|
|
62
|
+
const payload = {
|
|
63
|
+
event_config: {
|
|
64
|
+
event_name: 'Purchase',
|
|
65
|
+
show_fields: false
|
|
66
|
+
},
|
|
67
|
+
content_ids: ['product-123'],
|
|
68
|
+
value: 99.99,
|
|
69
|
+
userData: {
|
|
70
|
+
ph: '(555) 123-4567'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
75
|
+
|
|
76
|
+
// Phone should be cleaned of non-numeric characters then hashed
|
|
77
|
+
// '5551234567' -> '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c'
|
|
78
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
79
|
+
ph: '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c'
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should format userData with first and last name', async () => {
|
|
84
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
85
|
+
|
|
86
|
+
const payload = {
|
|
87
|
+
event_config: {
|
|
88
|
+
event_name: 'Purchase',
|
|
89
|
+
show_fields: false
|
|
90
|
+
},
|
|
91
|
+
content_ids: ['product-123'],
|
|
92
|
+
value: 99.99,
|
|
93
|
+
userData: {
|
|
94
|
+
fn: ' JOHN ',
|
|
95
|
+
ln: ' DOE '
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
100
|
+
|
|
101
|
+
// Names should be lowercased and trimmed, then hashed
|
|
102
|
+
// 'john' -> '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a'
|
|
103
|
+
// 'doe' -> '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f'
|
|
104
|
+
expect(mockFbq).toHaveBeenCalledWith(
|
|
105
|
+
'init',
|
|
106
|
+
'test-pixel-123',
|
|
107
|
+
{
|
|
108
|
+
fn: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a',
|
|
109
|
+
ln: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f'
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should format userData with gender', async () => {
|
|
115
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
116
|
+
|
|
117
|
+
const payload = {
|
|
118
|
+
event_config: {
|
|
119
|
+
event_name: 'Purchase',
|
|
120
|
+
show_fields: false
|
|
121
|
+
},
|
|
122
|
+
content_ids: ['product-123'],
|
|
123
|
+
value: 99.99,
|
|
124
|
+
userData: {
|
|
125
|
+
ge: 'm'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
130
|
+
|
|
131
|
+
// 'm' -> '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a'
|
|
132
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
133
|
+
ge: '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a'
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should format userData with date of birth', async () => {
|
|
138
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
139
|
+
|
|
140
|
+
const payload = {
|
|
141
|
+
event_config: {
|
|
142
|
+
event_name: 'Purchase',
|
|
143
|
+
show_fields: false
|
|
144
|
+
},
|
|
145
|
+
content_ids: ['product-123'],
|
|
146
|
+
value: 99.99,
|
|
147
|
+
userData: {
|
|
148
|
+
db: '1990-05-15T00:00:00.000Z'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
153
|
+
|
|
154
|
+
// Date should be formatted as YYYYMMDD then hashed
|
|
155
|
+
// '19900515' -> '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c'
|
|
156
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
157
|
+
db: '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c'
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should format userData with city', async () => {
|
|
162
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
163
|
+
|
|
164
|
+
const payload = {
|
|
165
|
+
event_config: {
|
|
166
|
+
event_name: 'Purchase',
|
|
167
|
+
show_fields: false
|
|
168
|
+
},
|
|
169
|
+
content_ids: ['product-123'],
|
|
170
|
+
value: 99.99,
|
|
171
|
+
userData: {
|
|
172
|
+
ct: ' New York '
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
177
|
+
|
|
178
|
+
// City should be lowercased with spaces removed, then hashed
|
|
179
|
+
// 'newyork' -> '350c754ba4d38897693aa077ef43072a859d23f613443133fecbbd90a3512ca5'
|
|
180
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
181
|
+
ct: '350c754ba4d38897693aa077ef43072a859d23f613443133fecbbd90a3512ca5'
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should format userData with US state - full name to code', async () => {
|
|
186
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
187
|
+
|
|
188
|
+
const payload = {
|
|
189
|
+
event_config: {
|
|
190
|
+
event_name: 'Purchase',
|
|
191
|
+
show_fields: false
|
|
192
|
+
},
|
|
193
|
+
content_ids: ['product-123'],
|
|
194
|
+
value: 99.99,
|
|
195
|
+
userData: {
|
|
196
|
+
st: 'California'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
201
|
+
|
|
202
|
+
// State should be converted to 2-letter code, then hashed
|
|
203
|
+
// 'ca' -> '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126'
|
|
204
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
205
|
+
st: '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126'
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should format userData with US state - already 2-letter code', async () => {
|
|
210
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
211
|
+
|
|
212
|
+
const payload = {
|
|
213
|
+
event_config: {
|
|
214
|
+
event_name: 'Purchase',
|
|
215
|
+
show_fields: false
|
|
216
|
+
},
|
|
217
|
+
content_ids: ['product-123'],
|
|
218
|
+
value: 99.99,
|
|
219
|
+
userData: {
|
|
220
|
+
st: 'NY'
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
225
|
+
|
|
226
|
+
// State code should be lowercased, then hashed
|
|
227
|
+
// 'ny' -> '1b06e2003f8420d6fa42badd8f77ec0f706b976b7a48b13c567dc5a559681683'
|
|
228
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
229
|
+
st: '1b06e2003f8420d6fa42badd8f77ec0f706b976b7a48b13c567dc5a559681683'
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should format userData with country - full name to code', async () => {
|
|
234
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
235
|
+
|
|
236
|
+
const payload = {
|
|
237
|
+
event_config: {
|
|
238
|
+
event_name: 'Purchase',
|
|
239
|
+
show_fields: false
|
|
240
|
+
},
|
|
241
|
+
content_ids: ['product-123'],
|
|
242
|
+
value: 99.99,
|
|
243
|
+
userData: {
|
|
244
|
+
country: 'United States'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
249
|
+
|
|
250
|
+
// Country should be converted to 2-letter code, then hashed
|
|
251
|
+
// 'us' -> '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621'
|
|
252
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
253
|
+
country: '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621'
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should format userData with country - already 2-letter code', async () => {
|
|
258
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
259
|
+
|
|
260
|
+
const payload = {
|
|
261
|
+
event_config: {
|
|
262
|
+
event_name: 'Purchase',
|
|
263
|
+
show_fields: false
|
|
264
|
+
},
|
|
265
|
+
content_ids: ['product-123'],
|
|
266
|
+
value: 99.99,
|
|
267
|
+
userData: {
|
|
268
|
+
country: 'GB'
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
273
|
+
|
|
274
|
+
// Country code should be lowercased, then hashed
|
|
275
|
+
// 'gb' -> '0b407281768f0e833afef47ed464b6571d01ca4d53c12ce5c51d1462f4ad6677'
|
|
276
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
277
|
+
country: '0b407281768f0e833afef47ed464b6571d01ca4d53c12ce5c51d1462f4ad6677'
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should format userData with zip code', async () => {
|
|
282
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
283
|
+
|
|
284
|
+
const payload = {
|
|
285
|
+
event_config: {
|
|
286
|
+
event_name: 'Purchase',
|
|
287
|
+
show_fields: false
|
|
288
|
+
},
|
|
289
|
+
content_ids: ['product-123'],
|
|
290
|
+
value: 99.99,
|
|
291
|
+
userData: {
|
|
292
|
+
zp: ' 94102 '
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
297
|
+
|
|
298
|
+
// Zip should be trimmed, then hashed
|
|
299
|
+
// '94102' -> '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31'
|
|
300
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
301
|
+
zp: '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31'
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should format userData with external_id', async () => {
|
|
306
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
307
|
+
|
|
308
|
+
const payload = {
|
|
309
|
+
event_config: {
|
|
310
|
+
event_name: 'Purchase',
|
|
311
|
+
show_fields: false
|
|
312
|
+
},
|
|
313
|
+
content_ids: ['product-123'],
|
|
314
|
+
value: 99.99,
|
|
315
|
+
userData: {
|
|
316
|
+
external_id: ' user-123 '
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
321
|
+
|
|
322
|
+
// External ID should be trimmed, then hashed
|
|
323
|
+
// 'user-123' -> 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8'
|
|
324
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', {
|
|
325
|
+
external_id: 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8'
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('should format userData with fbp and fbc cookies', async () => {
|
|
330
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
331
|
+
|
|
332
|
+
const payload = {
|
|
333
|
+
event_config: {
|
|
334
|
+
event_name: 'Purchase',
|
|
335
|
+
show_fields: false
|
|
336
|
+
},
|
|
337
|
+
content_ids: ['product-123'],
|
|
338
|
+
value: 99.99,
|
|
339
|
+
userData: {
|
|
340
|
+
fbp: ' fb.1.1234567890.1234567890 ',
|
|
341
|
+
fbc: ' fb.1.1234567890.AbCdEf123 '
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
346
|
+
|
|
347
|
+
// FBP and FBC should be trimmed but NOT hashed
|
|
348
|
+
expect(mockFbq).toHaveBeenCalledWith(
|
|
349
|
+
'init',
|
|
350
|
+
'test-pixel-123',
|
|
351
|
+
{
|
|
352
|
+
fbp: 'fb.1.1234567890.1234567890',
|
|
353
|
+
fbc: 'fb.1.1234567890.AbCdEf123'
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should format userData with all fields combined', async () => {
|
|
359
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
360
|
+
|
|
361
|
+
const payload = {
|
|
362
|
+
event_config: {
|
|
363
|
+
event_name: 'Purchase',
|
|
364
|
+
show_fields: false
|
|
365
|
+
},
|
|
366
|
+
content_ids: ['product-123'],
|
|
367
|
+
value: 99.99,
|
|
368
|
+
userData: {
|
|
369
|
+
external_id: 'user-123',
|
|
370
|
+
em: 'test@example.com',
|
|
371
|
+
ph: '5551234567',
|
|
372
|
+
fn: 'John',
|
|
373
|
+
ln: 'Doe',
|
|
374
|
+
ge: 'm',
|
|
375
|
+
db: '1990-05-15T00:00:00.000Z',
|
|
376
|
+
ct: 'San Francisco',
|
|
377
|
+
st: 'California',
|
|
378
|
+
zp: '94102',
|
|
379
|
+
country: 'United States',
|
|
380
|
+
fbp: 'fb.1.1234567890.1234567890',
|
|
381
|
+
fbc: 'fb.1.1234567890.AbCdEf123'
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
386
|
+
|
|
387
|
+
// All PII fields should be normalized and hashed; fbc/fbp should NOT be hashed
|
|
388
|
+
expect(mockFbq).toHaveBeenCalledWith(
|
|
389
|
+
'init',
|
|
390
|
+
'test-pixel-123',
|
|
391
|
+
{
|
|
392
|
+
external_id: 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8',
|
|
393
|
+
em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b',
|
|
394
|
+
ph: '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c',
|
|
395
|
+
fn: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a',
|
|
396
|
+
ln: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f',
|
|
397
|
+
ge: '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a',
|
|
398
|
+
db: '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c',
|
|
399
|
+
ct: '1a6bd4d9d79dc0a79b53795c70d3349fa9e38968a3fbefbfe8783efb1d2b6aac',
|
|
400
|
+
st: '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126',
|
|
401
|
+
zp: '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31',
|
|
402
|
+
country: '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621',
|
|
403
|
+
fbp: 'fb.1.1234567890.1234567890',
|
|
404
|
+
fbc: 'fb.1.1234567890.AbCdEf123'
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should skip invalid gender values', async () => {
|
|
410
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
411
|
+
|
|
412
|
+
const payload = {
|
|
413
|
+
event_config: {
|
|
414
|
+
event_name: 'Purchase',
|
|
415
|
+
show_fields: false
|
|
416
|
+
},
|
|
417
|
+
content_ids: ['product-123'],
|
|
418
|
+
value: 99.99,
|
|
419
|
+
userData: {
|
|
420
|
+
ge: 'invalid'
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
425
|
+
|
|
426
|
+
// Should not call init if only invalid gender is provided
|
|
427
|
+
const initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
428
|
+
expect(initCalls.length).toBe(0)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should skip invalid date of birth', async () => {
|
|
432
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
433
|
+
|
|
434
|
+
const payload = {
|
|
435
|
+
event_config: {
|
|
436
|
+
event_name: 'Purchase',
|
|
437
|
+
show_fields: false
|
|
438
|
+
},
|
|
439
|
+
content_ids: ['product-123'],
|
|
440
|
+
value: 99.99,
|
|
441
|
+
userData: {
|
|
442
|
+
db: 'invalid-date'
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
447
|
+
|
|
448
|
+
// Should not call init if only invalid date is provided
|
|
449
|
+
const initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
450
|
+
expect(initCalls.length).toBe(0)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('init call limits', () => {
|
|
455
|
+
it('should not send userData init when init count is at max', async () => {
|
|
456
|
+
mockAnalytics.storage.get.mockReturnValue('2')
|
|
457
|
+
|
|
458
|
+
const payload = {
|
|
459
|
+
event_config: {
|
|
460
|
+
event_name: 'Purchase',
|
|
461
|
+
show_fields: false
|
|
462
|
+
},
|
|
463
|
+
content_ids: ['product-123'],
|
|
464
|
+
value: 99.99,
|
|
465
|
+
userData: {
|
|
466
|
+
em: 'test@example.com'
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
471
|
+
|
|
472
|
+
// Should not call init with user data when count is at max
|
|
473
|
+
const initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
474
|
+
expect(initCalls.length).toBe(0)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('should only call init twice across multiple track events', async () => {
|
|
478
|
+
// Start with init count at 0
|
|
479
|
+
let currentInitCount = 0
|
|
480
|
+
mockAnalytics.storage.get.mockImplementation((key) => {
|
|
481
|
+
if (key === 'fb_pixel_init_count') {
|
|
482
|
+
return currentInitCount.toString()
|
|
483
|
+
}
|
|
484
|
+
return null
|
|
485
|
+
})
|
|
486
|
+
mockAnalytics.storage.set.mockImplementation((key, value) => {
|
|
487
|
+
if (key === 'fb_pixel_init_count') {
|
|
488
|
+
currentInitCount = parseInt(value, 10)
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
const payload = {
|
|
493
|
+
event_config: {
|
|
494
|
+
event_name: 'Purchase',
|
|
495
|
+
show_fields: false
|
|
496
|
+
},
|
|
497
|
+
content_ids: ['product-123'],
|
|
498
|
+
value: 99.99,
|
|
499
|
+
userData: {
|
|
500
|
+
em: 'test@example.com'
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// First track event - init should be called (count: 0 -> 1)
|
|
505
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
506
|
+
|
|
507
|
+
let initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
508
|
+
expect(initCalls.length).toBe(1)
|
|
509
|
+
expect(currentInitCount).toBe(1)
|
|
510
|
+
|
|
511
|
+
// Second track event - init should be called again (count: 1 -> 2)
|
|
512
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
513
|
+
|
|
514
|
+
initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
515
|
+
expect(initCalls.length).toBe(2)
|
|
516
|
+
expect(currentInitCount).toBe(2)
|
|
517
|
+
|
|
518
|
+
// Third track event - init should NOT be called (count stays at 2)
|
|
519
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
520
|
+
|
|
521
|
+
initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
522
|
+
expect(initCalls.length).toBe(2) // Still only 2 init calls
|
|
523
|
+
expect(currentInitCount).toBe(2) // Count remains at max
|
|
524
|
+
|
|
525
|
+
// Fourth track event - init should still NOT be called
|
|
526
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
527
|
+
|
|
528
|
+
initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
529
|
+
expect(initCalls.length).toBe(2) // Still only 2 init calls
|
|
530
|
+
expect(currentInitCount).toBe(2)
|
|
531
|
+
|
|
532
|
+
// Verify user data storage was still updated on all calls
|
|
533
|
+
const userDataSetCalls = (mockAnalytics.storage.set).mock.calls.filter((call) => call[0] === 'fb_user_data')
|
|
534
|
+
expect(userDataSetCalls.length).toBe(4) // User data stored on all 4 track events
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('should allow init to fire when count is reset to 0 (simulating new page load)', async () => {
|
|
538
|
+
const payload = {
|
|
539
|
+
event_config: {
|
|
540
|
+
event_name: 'Purchase',
|
|
541
|
+
show_fields: false
|
|
542
|
+
},
|
|
543
|
+
content_ids: ['product-123'],
|
|
544
|
+
value: 99.99,
|
|
545
|
+
userData: {
|
|
546
|
+
em: 'test@example.com'
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// === Previous Page Load Scenario ===
|
|
551
|
+
// Init count at max from previous events
|
|
552
|
+
mockAnalytics.storage.get.mockReturnValue('2')
|
|
553
|
+
|
|
554
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
555
|
+
|
|
556
|
+
let initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
557
|
+
expect(initCalls.length).toBe(0) // Init blocked when count at max
|
|
558
|
+
|
|
559
|
+
// === New Page Load Scenario ===
|
|
560
|
+
// On a new page load, the application would reset the init count to 0
|
|
561
|
+
// (In production, this happens when the page reloads and the pixel script re-initializes)
|
|
562
|
+
mockAnalytics.storage.get.mockReturnValue('0')
|
|
563
|
+
mockFbq.mockClear() // Simulate fresh page context
|
|
564
|
+
|
|
565
|
+
// First track event on new page
|
|
566
|
+
await send(mockFbq, mockClientParamBuilder, payload, defaultSettings, mockAnalytics)
|
|
567
|
+
|
|
568
|
+
initCalls = (mockFbq).mock.calls.filter((call) => call[0] === 'init')
|
|
569
|
+
expect(initCalls.length).toBe(1) // Init fires when count is reset
|
|
570
|
+
expect(mockFbq).toHaveBeenCalledWith('init', 'test-pixel-123', expect.objectContaining({
|
|
571
|
+
em: expect.any(String)
|
|
572
|
+
}))
|
|
573
|
+
|
|
574
|
+
// Verify init count was incremented in storage
|
|
575
|
+
expect(mockAnalytics.storage.set).toHaveBeenCalledWith('fb_pixel_init_count', '1')
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
})
|