fix(snap-conversions): send event_time in seconds behind feature flag - #3959
fix(snap-conversions): send event_time in seconds behind feature flag#3959mdkhan-tw wants to merge 2 commits into
Conversation
Snap's Conversions API OFFLINE endpoint interprets event_time as a Unix timestamp in seconds, but the destination sends milliseconds (Date.parse of the ISO timestamp), which Snap rejects with a 400 "Param data['event_time'] is an invalid Unix timestamp." (STRATCONN-6951, crunchfitness offline_leads). Add normalizeToUnixSeconds() and gate it behind the feature flag `snap-capi-event-time-in-seconds` (default off) so event_time is emitted in seconds. The magnitude guard (>= 1e12 => milliseconds) leaves values already in seconds untouched. Flag off preserves current behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a feature-flagged normalization so Snap Conversions API event_time can be sent as Unix seconds (instead of milliseconds) to avoid offline endpoint 400s for invalid timestamps.
Changes:
- Introduces
normalizeToUnixSeconds()with a millisecond-vs-second threshold guard. - Threads a new feature flag (
snap-capi-event-time-in-seconds) throughperformSnapCAPIv3→buildPayloadData()to conditionally normalizeevent_time. - Adds unit tests covering flag-on/flag-off behavior and the STRATCONN-6951 repro case.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/destination-actions/src/destinations/snap-conversions-api/reportConversionEvent/utils.ts | Adds normalizeToUnixSeconds() helper and threshold constant to convert ms → s safely. |
| packages/destination-actions/src/destinations/snap-conversions-api/reportConversionEvent/snap-capi-v3.ts | Adds feature flag constant and conditionally normalizes event_time during payload build. |
| packages/destination-actions/src/destinations/snap-conversions-api/tests/index.test.ts | Adds unit tests validating normalization on/off and numeric/ISO paths. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Snap's Conversions API expects `event_time` as a Unix timestamp in SECONDS (10 digits). | ||
| // `Date.parse` (and some upstream sources) produce MILLISECONDS (13 digits), which Snap's | ||
| // offline endpoint interprets as seconds far in the future and rejects with | ||
| // "Param data['event_time'] is an invalid Unix timestamp." Normalize milliseconds to seconds | ||
| // while leaving values already in seconds untouched. |
| describe('event_time normalization (snap-capi-event-time-in-seconds flag)', () => { | ||
| const FLAG = 'snap-capi-event-time-in-seconds' |
| // When enabled, `event_time` is normalized to a Unix timestamp in seconds (10 digits) as | ||
| // required by Snap's Conversions API, instead of the milliseconds (13 digits) produced by | ||
| // `Date.parse`. Gated for safe rollout on this high-volume destination. | ||
| export const FLAGON_EVENT_TIME_IN_SECONDS = 'snap-capi-event-time-in-seconds' |
- Import FLAGON_EVENT_TIME_IN_SECONDS in tests instead of re-declaring the flag string, so it can't drift from the implementation. - Reword normalizeToUnixSeconds comment to treat digit lengths as illustrative examples rather than normative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/destination-actions/src/destinations/snap-conversions-api/reportConversionEvent/snap-capi-v3.ts:23
FLAGON_EVENT_TIME_IN_SECONDSreads like a boolean rather than a flag key/name. Consider renaming to something that communicates it's an identifier (e.g.,FLAG_EVENT_TIME_IN_SECONDSorFEATURE_EVENT_TIME_IN_SECONDS) to reduce ambiguity for readers and callers.
// When enabled, `event_time` is normalized to a Unix timestamp in seconds (10 digits) as
// required by Snap's Conversions API, instead of the milliseconds (13 digits) produced by
// `Date.parse`. Gated for safe rollout on this high-volume destination.
export const FLAGON_EVENT_TIME_IN_SECONDS = 'snap-capi-event-time-in-seconds'
packages/destination-actions/src/destinations/snap-conversions-api/tests/index.test.ts:851
- This assertion is tightly coupled to the specific
testEvent.timestampvalue (and the exactDate.parseresult). To make the test less brittle, consider setting the event timestamp explicitly within this test (like the STRATCONN repro test does), or asserting properties that reflect the intended behavior (e.g.,>= 1e12when flag is off) rather than a single hard-coded millisecond value.
it('emits milliseconds (13 digits) when the flag is OFF (default, unchanged behavior)', async () => {
const { data } = await reportConversionEvent({
mapping: { event_type: 'PURCHASE', event_conversion_type: 'WEB' }
})
// Date.parse('2022-05-12T15:21:15.449Z') === 1652368875449 (milliseconds)
expect(data.event_time).toEqual(1652368875449)
})
… flag observability Reverts the staging-only forced-conversion hack (event_time is once again gated behind the snap-capi-event-time-in-seconds flag, matching PR #3959). Keeps the stats metric (snap_conversions.event_time_seconds with flag_received:<bool>) and info logs that report whether the flag is being delivered and the outgoing event_time value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Closing — the premise of this PR was a misdiagnosis. Live and production testing showed Snap's Conversions API accepts millisecond
The ms→seconds conversion does not fix the failures (e.g. Since Snap accepts both units, this change is at most a docs-alignment nicety and is not tied to the ticket, so closing rather than merging. |
Summary
Fixes STRATCONN-6951. Events from
crunchfitness/offline_leads_prodto the Snapchat Conversions API (actions-snap-conversions) fail delivery with:Root cause:
event_timedefaults to the eventtimestamp(ISO‑8601), which the destination parses withDate.parse()→ milliseconds (13 digits, e.g.1779305362702). Snap's OFFLINE endpoint interpretsevent_timeas seconds (10 digits), so a millisecond value resolves to a year far in the future and is rejected. Verified against a real failing payload:Date.parse("2026-05-20T19:29:22.702Z")=1779305362702; the valid value is1779305362.The millisecond behavior has existed since the v3 implementation (Apr 2024). It only surfaces as a hard 400 on the offline (RETL) path — the web/app pixel path tolerates milliseconds — which is why it presented as a single-customer issue rather than a fleet-wide regression (the downstream Snap‑400 metric is flat over the last 45 days).
Change
normalizeToUnixSeconds()inreportConversionEvent/utils.ts: divides by 1000 only when the value is>= 1e12(milliseconds), leaving values already in seconds untouched (no double‑division for customers who pass a 10‑digit numericevent_time).buildPayloadData()applies it, gated behind the feature flagsnap-capi-event-time-in-seconds(default off), threaded through fromperformSnapCAPIv3viadata.features.Note for the customer / rollout
These are backfilled offline leads (the sampled event is ~90 days old). Snap's offline attribution window is ~37 days, so months‑old events may still be rejected after this fix — but with a different error (out‑of‑window), not "invalid Unix timestamp." This PR resolves the timestamp‑format rejection only.
Testing
Unit tests added in
_tests_/index.test.tscovering: flag off (milliseconds, regression guard), flag on (ISO → seconds), the STRATCONN‑6951 offline repro (1779305362702→1779305362), and both numeric paths (13‑digit → converted, 10‑digit → untouched). All 25 snap‑conversions tests pass. No new/changed fields, so no breaking change.Stage testing
Failure case
Success case
🤖 Generated with Claude Code