@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,282 @@
1
+ import { formatFBEvent } from '../functions'
2
+ import { Payload } from '../generated-types'
3
+
4
+ describe('formatFBEvent', () => {
5
+ it('should format a complete Purchase event with all fields', () => {
6
+ const payload: Partial<Payload> = {
7
+ event_config: {
8
+ event_name: 'Purchase',
9
+ show_fields: true
10
+ },
11
+ content_ids: ['product-123', 'product-456'],
12
+ content_name: 'Test Product',
13
+ content_category: 'Electronics',
14
+ content_type: 'product',
15
+ contents: [
16
+ { id: 'product-123', quantity: 2, item_price: 49.99 },
17
+ { id: 'product-456', quantity: 1, item_price: 99.99 }
18
+ ],
19
+ currency: 'USD',
20
+ delivery_category: 'home_delivery',
21
+ num_items: 3,
22
+ value: 199.97,
23
+ predicted_ltv: 500.0,
24
+ net_revenue: 180.0,
25
+ custom_data: {
26
+ order_id: 'order-123',
27
+ campaign_id: 'summer-sale'
28
+ }
29
+ }
30
+
31
+ const result = formatFBEvent(payload as Payload)
32
+
33
+ expect(result).toEqual({
34
+ partner_agent: 'segment',
35
+ content_ids: ['product-123', 'product-456'],
36
+ content_name: 'Test Product',
37
+ content_category: 'Electronics',
38
+ content_type: 'product',
39
+ contents: [
40
+ { id: 'product-123', quantity: 2, item_price: 49.99 },
41
+ { id: 'product-456', quantity: 1, item_price: 99.99 }
42
+ ],
43
+ currency: 'USD',
44
+ delivery_category: 'home_delivery',
45
+ num_items: 3,
46
+ value: 199.97,
47
+ predicted_ltv: 500.0,
48
+ net_revenue: 180.0,
49
+ custom_data: {
50
+ order_id: 'order-123',
51
+ campaign_id: 'summer-sale'
52
+ }
53
+ })
54
+ })
55
+
56
+ it('should format minimal PageView event', () => {
57
+ const payload: Partial<Payload> = {
58
+ event_config: {
59
+ event_name: 'PageView',
60
+ show_fields: false
61
+ }
62
+ }
63
+
64
+ const result = formatFBEvent(payload as Payload)
65
+
66
+ expect(result).toEqual({
67
+ partner_agent: 'segment'
68
+ })
69
+ })
70
+
71
+ it('should include only provided fields', () => {
72
+ const payload: Partial<Payload> = {
73
+ event_config: {
74
+ event_name: 'ViewContent',
75
+ show_fields: true
76
+ },
77
+ content_ids: ['product-789'],
78
+ value: 149.99,
79
+ currency: 'USD'
80
+ }
81
+
82
+ const result = formatFBEvent(payload as Payload)
83
+
84
+ expect(result).toEqual({
85
+ partner_agent: 'segment',
86
+ content_ids: ['product-789'],
87
+ value: 149.99,
88
+ currency: 'USD'
89
+ })
90
+ })
91
+
92
+ it('should handle zero values for numeric fields', () => {
93
+ const payload: Partial<Payload> = {
94
+ event_config: {
95
+ event_name: 'Purchase',
96
+ show_fields: true
97
+ },
98
+ content_ids: ['product-123'],
99
+ value: 0,
100
+ num_items: 0,
101
+ predicted_ltv: 0,
102
+ net_revenue: 0
103
+ }
104
+
105
+ const result = formatFBEvent(payload as Payload)
106
+
107
+ expect(result).toEqual({
108
+ partner_agent: 'segment',
109
+ content_ids: ['product-123'],
110
+ value: 0,
111
+ num_items: 0,
112
+ predicted_ltv: 0,
113
+ net_revenue: 0
114
+ })
115
+ })
116
+
117
+ it('should not include empty arrays', () => {
118
+ const payload: Partial<Payload> = {
119
+ event_config: {
120
+ event_name: 'AddToCart',
121
+ show_fields: true
122
+ },
123
+ content_ids: [],
124
+ contents: [],
125
+ value: 99.99
126
+ }
127
+
128
+ const result = formatFBEvent(payload as Payload)
129
+
130
+ expect(result).toEqual({
131
+ partner_agent: 'segment',
132
+ value: 99.99
133
+ })
134
+ })
135
+
136
+ it('should not include empty custom_data object', () => {
137
+ const payload: Partial<Payload> = {
138
+ event_config: {
139
+ event_name: 'Purchase',
140
+ show_fields: true
141
+ },
142
+ content_ids: ['product-123'],
143
+ value: 99.99,
144
+ custom_data: {}
145
+ }
146
+
147
+ const result = formatFBEvent(payload as Payload)
148
+
149
+ expect(result).toEqual({
150
+ partner_agent: 'segment',
151
+ content_ids: ['product-123'],
152
+ value: 99.99
153
+ })
154
+ })
155
+
156
+ it('should include contents array with all item properties', () => {
157
+ const payload: Partial<Payload> = {
158
+ event_config: {
159
+ event_name: 'Purchase',
160
+ show_fields: true
161
+ },
162
+ contents: [
163
+ { id: 'product-1', quantity: 2, item_price: 25.50 },
164
+ { id: 'product-2', quantity: 1, item_price: 100.00 },
165
+ { id: 'product-3', quantity: 3 }
166
+ ],
167
+ value: 151.00
168
+ }
169
+
170
+ const result = formatFBEvent(payload as Payload)
171
+
172
+ expect(result).toEqual({
173
+ partner_agent: 'segment',
174
+ contents: [
175
+ { id: 'product-1', quantity: 2, item_price: 25.50 },
176
+ { id: 'product-2', quantity: 1, item_price: 100.00 },
177
+ { id: 'product-3', quantity: 3 }
178
+ ],
179
+ value: 151.00
180
+ })
181
+ })
182
+
183
+ it('should include all standard event fields', () => {
184
+ const payload: Partial<Payload> = {
185
+ event_config: {
186
+ event_name: 'InitiateCheckout',
187
+ show_fields: true
188
+ },
189
+ content_category: 'Apparel',
190
+ content_ids: ['shirt-123'],
191
+ content_name: 'Blue Shirt',
192
+ content_type: 'product',
193
+ currency: 'EUR',
194
+ num_items: 2,
195
+ value: 59.98
196
+ }
197
+
198
+ const result = formatFBEvent(payload as Payload)
199
+
200
+ expect(result).toEqual({
201
+ partner_agent: 'segment',
202
+ content_category: 'Apparel',
203
+ content_ids: ['shirt-123'],
204
+ content_name: 'Blue Shirt',
205
+ content_type: 'product',
206
+ currency: 'EUR',
207
+ num_items: 2,
208
+ value: 59.98
209
+ })
210
+ })
211
+
212
+ it('should format Subscribe event with predicted_ltv', () => {
213
+ const payload: Partial<Payload> = {
214
+ event_config: {
215
+ event_name: 'Subscribe',
216
+ show_fields: true
217
+ },
218
+ value: 9.99,
219
+ currency: 'USD',
220
+ predicted_ltv: 119.88
221
+ }
222
+
223
+ const result = formatFBEvent(payload as Payload)
224
+
225
+ expect(result).toEqual({
226
+ partner_agent: 'segment',
227
+ value: 9.99,
228
+ currency: 'USD',
229
+ predicted_ltv: 119.88
230
+ })
231
+ })
232
+
233
+ it('should format Purchase event with net_revenue', () => {
234
+ const payload: Partial<Payload> = {
235
+ event_config: {
236
+ event_name: 'Purchase',
237
+ show_fields: true
238
+ },
239
+ content_ids: ['product-123'],
240
+ value: 100.00,
241
+ currency: 'USD',
242
+ net_revenue: 85.00
243
+ }
244
+
245
+ const result = formatFBEvent(payload as Payload)
246
+
247
+ expect(result).toEqual({
248
+ partner_agent: 'segment',
249
+ content_ids: ['product-123'],
250
+ value: 100.00,
251
+ currency: 'USD',
252
+ net_revenue: 85.00
253
+ })
254
+ })
255
+
256
+ it('should include custom_data when provided', () => {
257
+ const payload: Partial<Payload> = {
258
+ event_config: {
259
+ event_name: 'Lead',
260
+ show_fields: true
261
+ },
262
+ value: 0,
263
+ custom_data: {
264
+ lead_source: 'facebook_ad',
265
+ lead_type: 'newsletter_signup',
266
+ campaign_name: 'Q1-2024'
267
+ }
268
+ }
269
+
270
+ const result = formatFBEvent(payload as Payload)
271
+
272
+ expect(result).toEqual({
273
+ partner_agent: 'segment',
274
+ value: 0,
275
+ custom_data: {
276
+ lead_source: 'facebook_ad',
277
+ lead_type: 'newsletter_signup',
278
+ campaign_name: 'Q1-2024'
279
+ }
280
+ })
281
+ })
282
+ })
@@ -0,0 +1,391 @@
1
+ import { formatUserData } from '../functions'
2
+ import { Payload } from '../generated-types'
3
+
4
+ describe('formatUserData', () => {
5
+ describe('without clientParamBuilder', () => {
6
+ it('should format and hash email', async () => {
7
+ const userData: Payload['userData'] = {
8
+ em: 'TEST@EXAMPLE.COM'
9
+ }
10
+
11
+ const result = await formatUserData(userData, undefined)
12
+
13
+ // Email should be normalized (lowercase, trimmed) and hashed
14
+ // 'test@example.com' -> '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
15
+ expect(result).toEqual({
16
+ em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
17
+ })
18
+ })
19
+
20
+ it('should format and hash phone number', async () => {
21
+ const userData: Payload['userData'] = {
22
+ ph: '(555) 123-4567'
23
+ }
24
+
25
+ const result = await formatUserData(userData, undefined)
26
+
27
+ // Phone should have non-numeric characters removed, then hashed
28
+ // '5551234567' -> '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c'
29
+ expect(result).toEqual({
30
+ ph: '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c'
31
+ })
32
+ })
33
+
34
+ it('should format and hash first and last name', async () => {
35
+ const userData: Payload['userData'] = {
36
+ fn: ' JOHN ',
37
+ ln: ' DOE '
38
+ }
39
+
40
+ const result = await formatUserData(userData, undefined)
41
+
42
+ // Names should be lowercased and trimmed, then hashed
43
+ // 'john' -> '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a'
44
+ // 'doe' -> '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f'
45
+ expect(result).toEqual({
46
+ fn: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a',
47
+ ln: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f'
48
+ })
49
+ })
50
+
51
+ it('should format and hash gender', async () => {
52
+ const userData: Payload['userData'] = {
53
+ ge: 'm'
54
+ }
55
+
56
+ const result = await formatUserData(userData, undefined)
57
+
58
+ // 'm' -> '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a'
59
+ expect(result).toEqual({
60
+ ge: '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a'
61
+ })
62
+ })
63
+
64
+ it('should format date of birth as YYYYMMDD and hash', async () => {
65
+ const userData: Payload['userData'] = {
66
+ db: '1990-05-15T00:00:00.000Z'
67
+ }
68
+
69
+ const result = await formatUserData(userData, undefined)
70
+
71
+ // Date formatted as '19900515' then hashed
72
+ // '19900515' -> '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c'
73
+ expect(result).toEqual({
74
+ db: '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c'
75
+ })
76
+ })
77
+
78
+ it('should format and hash city', async () => {
79
+ const userData: Payload['userData'] = {
80
+ ct: ' New York '
81
+ }
82
+
83
+ const result = await formatUserData(userData, undefined)
84
+
85
+ // City should be lowercased with spaces removed, then hashed
86
+ // 'newyork' -> '350c754ba4d38897693aa077ef43072a859d23f613443133fecbbd90a3512ca5'
87
+ expect(result).toEqual({
88
+ ct: '350c754ba4d38897693aa077ef43072a859d23f613443133fecbbd90a3512ca5'
89
+ })
90
+ })
91
+
92
+ it('should convert state name to code and hash', async () => {
93
+ const userData: Payload['userData'] = {
94
+ st: 'California'
95
+ }
96
+
97
+ const result = await formatUserData(userData, undefined)
98
+
99
+ // 'California' -> 'ca' -> hashed
100
+ // 'ca' -> '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126'
101
+ expect(result).toEqual({
102
+ st: '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126'
103
+ })
104
+ })
105
+
106
+ it('should lowercase state code and hash', async () => {
107
+ const userData: Payload['userData'] = {
108
+ st: 'NY'
109
+ }
110
+
111
+ const result = await formatUserData(userData, undefined)
112
+
113
+ // 'NY' -> 'ny' -> hashed
114
+ // 'ny' -> '1b06e2003f8420d6fa42badd8f77ec0f706b976b7a48b13c567dc5a559681683'
115
+ expect(result).toEqual({
116
+ st: '1b06e2003f8420d6fa42badd8f77ec0f706b976b7a48b13c567dc5a559681683'
117
+ })
118
+ })
119
+
120
+ it('should convert country name to code and hash', async () => {
121
+ const userData: Payload['userData'] = {
122
+ country: 'United States'
123
+ }
124
+
125
+ const result = await formatUserData(userData, undefined)
126
+
127
+ // 'United States' -> 'us' -> hashed
128
+ // 'us' -> '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621'
129
+ expect(result).toEqual({
130
+ country: '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621'
131
+ })
132
+ })
133
+
134
+ it('should format and hash zip code', async () => {
135
+ const userData: Payload['userData'] = {
136
+ zp: ' 94102 '
137
+ }
138
+
139
+ const result = await formatUserData(userData, undefined)
140
+
141
+ // Zip should be trimmed then hashed
142
+ // '94102' -> '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31'
143
+ expect(result).toEqual({
144
+ zp: '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31'
145
+ })
146
+ })
147
+
148
+ it('should format and hash external_id', async () => {
149
+ const userData: Payload['userData'] = {
150
+ external_id: ' user-123 '
151
+ }
152
+
153
+ const result = await formatUserData(userData, undefined)
154
+
155
+ // External ID should be trimmed then hashed
156
+ // 'user-123' -> 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8'
157
+ expect(result).toEqual({
158
+ external_id: 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8'
159
+ })
160
+ })
161
+
162
+ it('should NOT hash fbp and fbc cookies', async () => {
163
+ const userData: Payload['userData'] = {
164
+ fbp: ' fb.1.1234567890.1234567890 ',
165
+ fbc: ' fb.1.1234567890.AbCdEf123 '
166
+ }
167
+
168
+ const result = await formatUserData(userData, undefined)
169
+
170
+ // FBP and FBC should only be trimmed, NOT hashed
171
+ expect(result).toEqual({
172
+ fbp: 'fb.1.1234567890.1234567890',
173
+ fbc: 'fb.1.1234567890.AbCdEf123'
174
+ })
175
+ })
176
+
177
+ it('should format all fields combined', async () => {
178
+ const userData: Payload['userData'] = {
179
+ external_id: 'user-123',
180
+ em: 'test@example.com',
181
+ ph: '5551234567',
182
+ fn: 'John',
183
+ ln: 'Doe',
184
+ ge: 'm',
185
+ db: '1990-05-15T00:00:00.000Z',
186
+ ct: 'San Francisco',
187
+ st: 'California',
188
+ zp: '94102',
189
+ country: 'United States',
190
+ fbp: 'fb.1.1234567890.1234567890',
191
+ fbc: 'fb.1.1234567890.AbCdEf123'
192
+ }
193
+
194
+ const result = await formatUserData(userData, undefined)
195
+
196
+ expect(result).toEqual({
197
+ external_id: 'fcdec6df4d44dbc637c7c5b58efface52a7f8a88535423430255be0bb89bedd8',
198
+ em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b',
199
+ ph: '3c95277da5fd0da6a1a44ee3fdf56d20af6c6d242695a40e18e6e90dc3c5872c',
200
+ fn: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a',
201
+ ln: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f',
202
+ ge: '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a',
203
+ db: '53058fbd6731774c37a6d838c09d25b337fa7b9b5007f82cc934d857d2596e0c',
204
+ ct: '1a6bd4d9d79dc0a79b53795c70d3349fa9e38968a3fbefbfe8783efb1d2b6aac',
205
+ st: '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126',
206
+ zp: '8137c19c8f35f6b6a1cce99753226e1c7211eaaebd68528b789f973b0be95e31',
207
+ country: '79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621',
208
+ fbp: 'fb.1.1234567890.1234567890',
209
+ fbc: 'fb.1.1234567890.AbCdEf123'
210
+ })
211
+ })
212
+
213
+ it('should return undefined when userData is undefined', async () => {
214
+ const result = await formatUserData(undefined, undefined)
215
+
216
+ expect(result).toBeUndefined()
217
+ })
218
+
219
+ it('should return undefined when all fields are invalid', async () => {
220
+ const userData: Payload['userData'] = {
221
+ ge: 'invalid'
222
+ }
223
+
224
+ const result = await formatUserData(userData, undefined)
225
+
226
+ expect(result).toBeUndefined()
227
+ })
228
+
229
+ it('should skip invalid gender values', async () => {
230
+ const userData: Payload['userData'] = {
231
+ em: 'test@example.com',
232
+ ge: 'invalid'
233
+ }
234
+
235
+ const result = await formatUserData(userData, undefined)
236
+
237
+ // Should only include email, skip invalid gender
238
+ expect(result).toEqual({
239
+ em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
240
+ })
241
+ })
242
+
243
+ it('should skip invalid date of birth', async () => {
244
+ const userData: Payload['userData'] = {
245
+ em: 'test@example.com',
246
+ db: 'invalid-date'
247
+ }
248
+
249
+ const result = await formatUserData(userData, undefined)
250
+
251
+ // Should only include email, skip invalid date
252
+ expect(result).toEqual({
253
+ em: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b'
254
+ })
255
+ })
256
+ })
257
+
258
+ describe('with clientParamBuilder', () => {
259
+ let mockClientParamBuilder: any
260
+
261
+ beforeEach(() => {
262
+ mockClientParamBuilder = {
263
+ getNormalizedAndHashedPII: jest.fn(),
264
+ processAndCollectAllParams: jest.fn(),
265
+ getFbc: jest.fn(),
266
+ getFbp: jest.fn()
267
+ }
268
+ })
269
+
270
+ it('should use clientParamBuilder for email', async () => {
271
+ mockClientParamBuilder.getNormalizedAndHashedPII.mockReturnValue('hashed_email_value')
272
+
273
+ const userData: Payload['userData'] = {
274
+ em: 'TEST@EXAMPLE.COM'
275
+ }
276
+
277
+ const result = await formatUserData(userData, mockClientParamBuilder)
278
+
279
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('TEST@EXAMPLE.COM', 'email')
280
+ expect(result).toEqual({
281
+ em: 'hashed_email_value'
282
+ })
283
+ })
284
+
285
+ it('should use clientParamBuilder for all PII fields', async () => {
286
+ mockClientParamBuilder.getNormalizedAndHashedPII.mockImplementation((_, type) => {
287
+ return `hashed_${type}_value`
288
+ })
289
+
290
+ const userData: Payload['userData'] = {
291
+ em: 'test@example.com',
292
+ ph: '5551234567',
293
+ fn: 'John',
294
+ ln: 'Doe',
295
+ ge: 'm',
296
+ db: '1990-05-15',
297
+ ct: 'San Francisco',
298
+ st: 'CA',
299
+ zp: '94102',
300
+ country: 'US',
301
+ external_id: 'user-123'
302
+ }
303
+
304
+ const result = await formatUserData(userData, mockClientParamBuilder)
305
+
306
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('test@example.com', 'email')
307
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('5551234567', 'phone')
308
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('John', 'first_name')
309
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('Doe', 'last_name')
310
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('m', 'gender')
311
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('1990-05-15', 'date_of_birth')
312
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('San Francisco', 'city')
313
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('CA', 'state')
314
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('94102', 'zip_code')
315
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('US', 'country')
316
+ expect(mockClientParamBuilder.getNormalizedAndHashedPII).toHaveBeenCalledWith('user-123', 'external_id')
317
+
318
+ expect(result).toEqual({
319
+ em: 'hashed_email_value',
320
+ ph: 'hashed_phone_value',
321
+ fn: 'hashed_first_name_value',
322
+ ln: 'hashed_last_name_value',
323
+ ge: 'hashed_gender_value',
324
+ db: 'hashed_date_of_birth_value',
325
+ ct: 'hashed_city_value',
326
+ st: 'hashed_state_value',
327
+ zp: 'hashed_zip_code_value',
328
+ country: 'hashed_country_value',
329
+ external_id: 'hashed_external_id_value'
330
+ })
331
+ })
332
+
333
+ it('should use clientParamBuilder getFbc and getFbp methods', async () => {
334
+ mockClientParamBuilder.getNormalizedAndHashedPII.mockReturnValue('hashed_email')
335
+ mockClientParamBuilder.getFbc.mockReturnValue('fb.1.1234567890.ClientParamBuilderFbc')
336
+ mockClientParamBuilder.getFbp.mockReturnValue('fb.1.1234567890.ClientParamBuilderFbp')
337
+
338
+ const userData: Payload['userData'] = {
339
+ em: 'test@example.com',
340
+ fbc: 'fb.1.1234567890.PayloadFbc',
341
+ fbp: 'fb.1.1234567890.PayloadFbp'
342
+ }
343
+
344
+ const result = await formatUserData(userData, mockClientParamBuilder)
345
+
346
+ expect(mockClientParamBuilder.processAndCollectAllParams).toHaveBeenCalled()
347
+ expect(mockClientParamBuilder.getFbc).toHaveBeenCalled()
348
+ expect(mockClientParamBuilder.getFbp).toHaveBeenCalled()
349
+
350
+ // ClientParamBuilder values should override payload values
351
+ expect(result).toEqual({
352
+ em: 'hashed_email',
353
+ fbc: 'fb.1.1234567890.ClientParamBuilderFbc',
354
+ fbp: 'fb.1.1234567890.ClientParamBuilderFbp'
355
+ })
356
+ })
357
+
358
+ it('should fallback to payload fbc/fbp when clientParamBuilder returns null', async () => {
359
+ mockClientParamBuilder.getNormalizedAndHashedPII.mockReturnValue('hashed_email')
360
+ mockClientParamBuilder.getFbc.mockReturnValue(null)
361
+ mockClientParamBuilder.getFbp.mockReturnValue(null)
362
+
363
+ const userData: Payload['userData'] = {
364
+ em: 'test@example.com',
365
+ fbc: 'fb.1.1234567890.PayloadFbc',
366
+ fbp: 'fb.1.1234567890.PayloadFbp'
367
+ }
368
+
369
+ const result = await formatUserData(userData, mockClientParamBuilder)
370
+
371
+ expect(result).toEqual({
372
+ em: 'hashed_email',
373
+ fbc: 'fb.1.1234567890.PayloadFbc',
374
+ fbp: 'fb.1.1234567890.PayloadFbp'
375
+ })
376
+ })
377
+
378
+ it('should return undefined when clientParamBuilder returns undefined for all fields', async () => {
379
+ mockClientParamBuilder.getNormalizedAndHashedPII.mockReturnValue(undefined)
380
+
381
+ const userData: Payload['userData'] = {
382
+ em: 'test@example.com',
383
+ ph: '5551234567'
384
+ }
385
+
386
+ const result = await formatUserData(userData, mockClientParamBuilder)
387
+
388
+ expect(result).toBeUndefined()
389
+ })
390
+ })
391
+ })