11import { createHash } from 'node:crypto'
22import { describe , expect , test } from 'bun:test'
33
4+ import { hashMatchingEmail } from '../matching-hash'
45import {
56 buildMetaConversionBody ,
67 metaConversionId ,
78 sendMetaConversion ,
89 type SendMetaConversionParams ,
910} from '../meta-capi'
1011import {
12+ metaClickCookieValue ,
1113 metaTrackingOptedOut ,
1214 validMetaBrowserId ,
1315} from '../util/meta-conversions'
1416
17+ const hashedEmail = hashMatchingEmail ( ' Person@Example.com ' ) !
1518const params : SendMetaConversionParams = {
1619 pixelId : '123456789' ,
1720 accessToken : 'secret-never-in-body' ,
@@ -23,6 +26,8 @@ const params: SendMetaConversionParams = {
2326 attribution : {
2427 fbc : 'fb.1.1790000000000.click' ,
2528 fbp : 'fb.1.1790000000000.1234' ,
29+ hashedEmail,
30+ ipAddress : '203.0.113.9' ,
2631 userAgent : 'Browser' ,
2732 } ,
2833}
@@ -59,28 +64,47 @@ describe('Meta conversion payload and attribution', () => {
5964 ) . toThrow ( 'positive confirmed USD payment' )
6065 }
6166 } )
62- test ( 'uses seconds and hashed canonical identity, without email, URL parameters, or token' , ( ) => {
67+ test ( 'uses seconds, hashed identity and every enrollment match key, never the raw email, URL parameters or token' , ( ) => {
6368 const body = buildMetaConversionBody ( params )
6469 expect ( body . data [ 0 ] ?. event_time ) . toBe ( 1789907420 )
65- expect ( body . data [ 0 ] ?. user_data . external_id ) . toEqual ( [
66- createHash ( 'sha256' ) . update ( 'canonical-user' ) . digest ( 'hex' ) ,
67- ] )
68- expect ( body . data [ 0 ] ?. user_data . fbc ) . toBe ( params . attribution . fbc )
70+ expect ( body . data [ 0 ] ?. user_data ) . toEqual ( {
71+ external_id : [
72+ createHash ( 'sha256' ) . update ( 'canonical-user' ) . digest ( 'hex' ) ,
73+ ] ,
74+ em : [ hashedEmail ] ,
75+ fbc : params . attribution . fbc ,
76+ fbp : params . attribution . fbp ,
77+ client_ip_address : '203.0.113.9' ,
78+ client_user_agent : 'Browser' ,
79+ } )
6980 expect ( body . data [ 0 ] ?. action_source ) . toBe ( 'website' )
7081 expect ( body . data [ 0 ] ?. event_source_url ) . toBe ( 'https://freebuff.com/' )
71- expect ( JSON . stringify ( body ) ) . not . toContain ( 'canonical-user' )
72- expect ( JSON . stringify ( body ) ) . not . toContain ( 'secret-never-in-body' )
73- expect ( body . data [ 0 ] ?. user_data ) . not . toHaveProperty ( 'em' )
82+ const serialized = JSON . stringify ( body )
83+ expect ( serialized ) . not . toContain ( 'canonical-user' )
84+ expect ( serialized ) . not . toContain ( 'secret-never-in-body' )
85+ expect ( serialized . toLowerCase ( ) ) . not . toContain ( 'person@example.com' )
86+ } )
87+ test ( 'omits match keys it does not have rather than sending empty values' , ( ) => {
88+ const userData = buildMetaConversionBody ( {
89+ ...params ,
90+ attribution : { fbp : params . attribution . fbp , userAgent : 'Browser' } ,
91+ } ) . data [ 0 ] ?. user_data
92+ expect ( userData ) . not . toHaveProperty ( 'em' )
93+ expect ( userData ) . not . toHaveProperty ( 'fbc' )
94+ expect ( userData ) . not . toHaveProperty ( 'client_ip_address' )
95+ expect ( userData ?. client_user_agent ) . toBe ( 'Browser' )
7496 } )
75- test ( 'reports native coding as other, without a fictional website event or browser user agent ' , ( ) => {
97+ test ( 'reports native coding as other, without a fictional website event, but with the same person match keys ' , ( ) => {
7698 const event = buildMetaConversionBody ( {
7799 ...params ,
78100 eventName : 'CodingActivation' ,
79101 surface : 'desktop' ,
80102 } ) . data [ 0 ]
81103 expect ( event ?. action_source ) . toBe ( 'other' )
82104 expect ( event ) . not . toHaveProperty ( 'event_source_url' )
83- expect ( event ?. user_data ) . not . toHaveProperty ( 'client_user_agent' )
105+ expect ( event ?. user_data . client_user_agent ) . toBe ( 'Browser' )
106+ expect ( event ?. user_data . client_ip_address ) . toBe ( '203.0.113.9' )
107+ expect ( event ?. user_data . em ) . toEqual ( [ hashedEmail ] )
84108 expect ( metaConversionId ( 'CompleteRegistration' , params . userId ) ) . not . toBe (
85109 metaConversionId ( 'CodingActivation' , params . userId ) ,
86110 )
@@ -100,6 +124,18 @@ describe('Meta conversion payload and attribution', () => {
100124 expect ( metaTrackingOptedOut ( new Headers ( { DNT : '1' } ) ) ) . toBe ( true )
101125 expect ( metaTrackingOptedOut ( new Headers ( ) ) ) . toBe ( false )
102126 } )
127+ test ( "builds a first-party _fbc in Meta's own shape that the server-side validator accepts" , ( ) => {
128+ const value = metaClickCookieValue ( 'IwAR0abc_-123' , 1790000000000.7 )
129+ expect ( value ) . toBe ( 'fb.1.1790000000000.IwAR0abc_-123' )
130+ expect ( validMetaBrowserId ( value ) ) . toBe ( value )
131+ for ( const clickId of [ '' , 'not a click' , 'a' . repeat ( 401 ) , undefined ] )
132+ expect ( metaClickCookieValue ( clickId , 1790000000000 ) ) . toBeUndefined ( )
133+ } )
134+ test ( 'normalizes the email before hashing and refuses anything that is not one' , ( ) => {
135+ expect ( hashMatchingEmail ( 'person@example.com' ) ) . toBe ( hashedEmail )
136+ for ( const value of [ undefined , null , 1 , '' , 'not-email' , 'a b@c.com' ] )
137+ expect ( hashMatchingEmail ( value ) ) . toBeUndefined ( )
138+ } )
103139} )
104140
105141describe ( 'Meta conversion transport' , ( ) => {
0 commit comments