@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.
@@ -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
+ })