A purchase recorded at noon can appear hours, years or decades away from its intended time when one adapter guesses the unit of a field called timestamp. The JSON may remain perfectly parseable throughout the journey.
The reliable fix starts before the HTTP request. Choose an internal representation, convert it explicitly for each endpoint and test the serialized output with a fixed instant. Then check what the destination actually recorded.
This walkthrough builds a small Node.js contract test. It does not send events or require vendor credentials.
Give the internal timestamp a unit
Use a name such as occurred_at_ms if the application stores epoch milliseconds. Include the unit in the schema and function contract. A caller should not need to count digits to understand a value.
Keep occurrence time separate from the time your worker received or transmitted the event. Retries may change transmission time. They should not rewrite when the underlying purchase happened.
For the endpoints covered here, Meta server events use epoch seconds, GA4 Measurement Protocol uses timestamp_micros, and the referenced PostHog capture and Segment HTTP contracts accept ISO 8601 timestamp strings. Verify the endpoint you actually use. A vendor may operate several APIs with different contracts.
Use a fixed instant with fractional seconds
An input ending at exactly midnight can hide rounding and precision mistakes. Use an instant with a nonzero fractional second and compare exact expected values.
Save the following as timestamp-contract.mjs and run node timestamp-contract.mjs:
import assert from 'node:assert/strict';
function adaptOccurredAtMs(milliseconds) {
if (!Number.isSafeInteger(milliseconds)) {
throw new TypeError('occurred_at_ms must be a safe integer');
}
const date = new Date(milliseconds);
if (!Number.isFinite(date.getTime())) {
throw new RangeError('occurred_at_ms is outside the Date range');
}
const micros = milliseconds * 1000;
if (!Number.isSafeInteger(micros)) {
throw new RangeError('microsecond representation is not exact');
}
return {
meta: { event_time: Math.floor(milliseconds / 1000) },
ga4: { timestamp_micros: micros },
posthog: { timestamp: date.toISOString() },
segment: { timestamp: date.toISOString() }
};
}
const milliseconds = Date.parse('2026-10-01T12:34:56.789Z');
const wire = JSON.parse(JSON.stringify(adaptOccurredAtMs(milliseconds)));
assert.equal(wire.meta.event_time, 1790858096);
assert.equal(wire.ga4.timestamp_micros, 1790858096789000);
assert.equal(wire.posthog.timestamp, '2026-10-01T12:34:56.789Z');
assert.equal(wire.segment.timestamp, '2026-10-01T12:34:56.789Z');
assert.equal(typeof wire.meta.event_time, 'number');
assert.equal(typeof wire.ga4.timestamp_micros, 'number');
assert.throws(() => adaptOccurredAtMs(String(milliseconds)), TypeError);
assert.throws(() => adaptOccurredAtMs(NaN), TypeError);
assert.throws(() => adaptOccurredAtMs(Infinity), TypeError);
assert.throws(() => adaptOccurredAtMs(milliseconds + 0.5), TypeError);
assert.throws(() => adaptOccurredAtMs(Number.MAX_SAFE_INTEGER), RangeError);
The function intentionally accepts only an integer number. It does not silently coerce strings, infer units or attempt to repair a malformed value.
The microsecond safety check is stricter than JavaScript's Date range. That is deliberate: a representable millisecond value does not guarantee that multiplying it by one thousand produces an exactly represented integer.
Precision conversion has a direction
The Meta conversion discards the fractional second. For this fixture, the difference between the source instant and the converted seconds expressed as milliseconds is 789 milliseconds. Document that loss as part of the adapter contract.
The GA4 conversion changes the unit. It does not create measurement precision. An input measured to milliseconds still has millisecond precision after you append three decimal zeros to its integer representation.
If the source genuinely records microseconds, preserve that representation upstream and design a separate adapter. Passing it through Date first would discard information before the GA4 payload is built.
Likewise, toISOString() produces a UTC representation of the instant. Formatting an instant in a local timezone for a user interface is a separate operation. Do not feed that display string back into your event adapter as if it were the canonical event time.
A number cannot tell you whether it is seconds
The adapter rejects a numeric string, but a seconds value passed as an integer can still be a valid JavaScript date in milliseconds. The arithmetic cannot determine the caller's intent.
That is why the input field needs a unit and why the caller needs its own test. At the boundary where your application reads a database record or message, assert that the chosen source column is the millisecond column.
For a synthetic purchase, keep both the fixed source record and the expected endpoint payload in the fixture. Change the input deliberately to seconds and confirm that the caller's expectation fails. Do not add a digit-count heuristic that guesses the intended date and makes the broken producer harder to find.
If your business accepts events only within a particular period, add that as a named policy check. Keep it separate from the conversion function. A business window and a vendor ingestion window are different contracts and may change independently.
Check the bytes after serialization
The test serializes and parses the adapter result before asserting types. This catches changes introduced by your payload construction, such as converting event_time into a string inside a template.
For a real integration, run the same assertions at the last observable boundary before sending. If an HTTP client performs additional transformations, capture the request body with an injected test transport. Testing an earlier object can miss changes made by middleware.
Add fixtures for an absent field, an invalid source date and a value outside your supported application range. Decide whether a missing occurrence time should reject the event or follow an explicitly documented fallback. Calling Date.now() without recording that decision can make a replay appear to be a new purchase.
Verify the destination with a synthetic event
Local tests establish your conversion and serialization behavior. They do not prove that the endpoint accepted the timestamp or used it in reporting.
For an integration test, submit one permitted synthetic event in a test environment. Record the source instant, endpoint, send time and destination observation. If the observed event time differs, inspect the adapter output before assuming the reporting interface is displaying the wrong timezone.
Also inspect whether the destination substituted ingestion time because a field was absent, ignored or outside its accepted window. Preserve the actual response and relevant diagnostics so that the result can be reproduced later.
I maintain Pixellint. Its cross-vendor payload reference helps review the endpoint contract after these arithmetic tests pass. Keep the small timestamp tests in your own codebase: they identify the exact conversion that changed when an adapter regresses.
Sources
Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.
Top comments (0)