Skip to content

The dual-send invariant

Identical input to normalization + hashing must produce byte-identical output on browser and server.

This is the single most important rule in Trackbridge. If it ever breaks, the SDK has silently failed — Google won’t dedupe conversions, users will be double-counted, and there will be no error message anywhere to point at the cause.

Trackbridge’s value proposition is dual-send: fire the same conversion from browser and server, let Google dedupe via shared transactionId, never lose a conversion. For Google’s matching to work, the hashed user data must be byte-identical on both sides. If one side hashes Jane@Example.com as the lowercase trimmed version and the other side accidentally trims differently or normalizes a unicode character differently, the match fails. Google won’t tell you. The conversion just won’t be enhanced and your match rate looks lower than it should.

Subtle places normalization can drift between runtimes:

  • Whitespace handling. Different trim() implementations across older Node versions vs browsers (mostly fine in modern engines, but worth pinning behavior in tests).
  • Unicode normalization. é can be a single code point or e + combining mark. Always normalize to NFC before hashing.
  • Phone number parsing. If browser uses one library and server uses another, even trivial differences (how they handle extensions, lead zeros, country code inference) will diverge.
  • Address normalization. Punctuation stripping, abbreviation handling (Street vs St), country code conversion. Google expects ISO-3166-1 alpha-2.
  • Hashing. SHA-256 itself is fine, but the encoding of the input bytes matters. Always hash the UTF-8 byte representation, never the JS string directly.

1. All normalization lives in @trackbridge/core

Section titled “1. All normalization lives in @trackbridge/core”

The browser and server packages never implement their own normalization. They import from core. There must be one and only one normalizeEmail, normalizePhone, normalizeName, normalizeAddress, hashSha256 in the entire codebase.

packages/core/src/__tests__/golden.test.ts holds canonical input/output pairs:

test('email normalization is stable', () => {
expect(normalizeEmail(' Jane@Example.COM ')).toBe('jane@example.com');
});
test('email hash is pinned', () => {
// If this hash ever changes, dual-send breaks for every existing user.
// Bumping it is a MAJOR version bump.
expect(hashEmail('jane@example.com')).toBe(
'a8b3...e91f', // real value pinned in the test file
);
});

Any change to a normalization function that alters a golden output is a breaking change, full stop. It requires a major version bump and a migration note for users.

The CI matrix runs the core test suite under both Node and a browser-like environment (jsdom or happy-dom is fine for hashing tests; the inputs and outputs are deterministic). If golden tests pass under Node but fail under jsdom, something runtime-specific has leaked into core.

4. @trackbridge/core has zero runtime dependencies

Section titled “4. @trackbridge/core has zero runtime dependencies”

External libraries are the most common source of subtle behavior drift. Core uses only the JS standard library: TextEncoder, crypto.subtle.digest (browser) / node:crypto (server). Phone parsing uses libraries only in @trackbridge/browser and @trackbridge/server if at all — but if it does, it must be the same library, same version, applied identically. The preferred approach: a minimal E.164 normalizer in core, behavior pinned by golden tests.

If you’re adding a new field to userData (say, dateOfBirth per Google’s spec for some conversion types):

  1. Add the normalization function to @trackbridge/core
  2. Add golden tests with at least 5 canonical inputs and their expected outputs
  3. Add the field to the type definitions
  4. Update both browser and server to use the new normalizer — never inline anything
  5. Verify golden tests pass on both runtimes in CI

If you’re tempted to “just lowercase this real quick” inline somewhere outside core: don’t. Add it to core with a test.