Skip to content

fix(snap-conversions): send event_time in seconds behind feature flag - #3959

Closed
mdkhan-tw wants to merge 2 commits into
mainfrom
fix/stratconn-6951-snap-event-time-seconds
Closed

fix(snap-conversions): send event_time in seconds behind feature flag#3959
mdkhan-tw wants to merge 2 commits into
mainfrom
fix/stratconn-6951-snap-event-time-seconds

Conversation

@mdkhan-tw

@mdkhan-tw mdkhan-tw commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes STRATCONN-6951. Events from crunchfitness / offline_leads_prod to the Snapchat Conversions API (actions-snap-conversions) fail delivery with:

400 { "status": "INVALID", "error_msgs": ["Param data['event_time'] is an invalid Unix timestamp."] }

Root cause: event_time defaults to the event timestamp (ISO‑8601), which the destination parses with Date.parse()milliseconds (13 digits, e.g. 1779305362702). Snap's OFFLINE endpoint interprets event_time as 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 is 1779305362.

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

  • New normalizeToUnixSeconds() in reportConversionEvent/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 numeric event_time).
  • buildPayloadData() applies it, gated behind the feature flag snap-capi-event-time-in-seconds (default off), threaded through from performSnapCAPIv3 via data.features.
  • Flag off preserves current behavior — no change for existing customers. Gated for safe rollout on this high‑volume destination.

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

  • Added unit tests for new functionality
  • Tested end-to-end using the local server
  • [If destination is already live] Tested for backward compatibility of destination. Note: New required fields are a breaking change.

Unit tests added in _tests_/index.test.ts covering: flag off (milliseconds, regression guard), flag on (ISO → seconds), the STRATCONN‑6951 offline repro (17793053627021779305362), 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

{
  "body": {
    "data": [
      {
        "action_source": "website",
        "event_id": "022bb90c-bbac-11e4-8dfc-aa07a5b093db",
        "event_name": "PURCHASE",
        "event_source_url": "https://segment.com/docs/connections/spec/common/",
        "event_time": 1786334911,
        "integration": "segment",
        "user_data": {
          "client_ip_address": "8.8.8.8",
          "client_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
          "external_id": [
            "f676a029005e48e2e87a1dfaa2c01b8d51d2d021978228decbf19fe152d21c73"
          ]
        }
      }
    ]
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip,deflate",
    "Authorization": "REDACTED",
    "Connection": "close",
    "Content-Length": "502",
    "Content-Type": "application/json",
    "Host": "tr.snapchat.com",
    "User-Agent": "REDACTED"
  }
}

{
  "body": {
    "reason": "Events have been processed successfully.",
    "status": "VALID"
  },
  "headers": {
    "Alt-Svc": "h3=\":443\"; ma=2592000",
    "Connection": "close",
    "Content-Length": "70",
    "Content-Type": "application/json",
    "Date": "Mon, 24 Aug 2026 09:09:53 GMT",
    "Server": "API Gateway",
    "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload, max-age=31536000; includeSubDomains",
    "Via": "1.1 google",
    "X-Envoy-Upstream-Service-Time": "3"
  }
}
Screenshot 2026-08-24 at 2 41 29 PM

🤖 Generated with Claude Code

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>
Copilot AI lite review requested due to automatic review settings August 19, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) through performSnapCAPIv3buildPayloadData() to conditionally normalize event_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.

Comment on lines +73 to +77
// 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.
Comment on lines +842 to +843
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>
Copilot AI review requested due to automatic review settings August 19, 2026 07:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_SECONDS reads 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_SECONDS or FEATURE_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.timestamp value (and the exact Date.parse result). 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., >= 1e12 when 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)
    })

mdkhan-tw added a commit that referenced this pull request Aug 24, 2026
… 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>
@mdkhan-tw

Copy link
Copy Markdown
Contributor Author

Closing — the premise of this PR was a misdiagnosis.

Live and production testing showed Snap's Conversions API accepts millisecond event_time; acceptance tracks the timestamp's age, not its format. Snap reports out-of-attribution-window timestamps with the same "invalid Unix timestamp" message.

event_time format date age at send Snap result
1786334911905 ms 2026-08-10 15 d ✅ VALID/200
1780169592302 (prod) ms 2026-05-30 86 d ❌ 400
1779305362702 (orig) ms 2026-05-20 89 d ❌ 400
1702181311 sec 2023-12-10 988 d ❌ 400

The ms→seconds conversion does not fix the failures (e.g. 1780169592302 ms → 1780169592 s is the same 86-day-old instant, still outside Snap's ~37-day offline window). Real root cause: RETL-backfilled offline leads whose event_time is months old. Details on STRATCONN-6951.

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.

@mdkhan-tw mdkhan-tw closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants