Skip to content

Report OpenPanel delivery failures instead of dropping them - #34

Merged
multiplehats merged 1 commit into
mainfrom
fix/openpanel-delivery-failures
Sep 5, 2026
Merged

multiplehats merged 1 commit into
mainfrom
fix/openpanel-delivery-failures

Conversation

@multiplehats

@multiplehats multiplehats commented Sep 3, 2026

Copy link
Copy Markdown
Owner

The problem

OpenPanel's SDKs treat HTTP 401 as a non-error. Their shared Api.post does:

if (response.status === 401) return null;

No throw, no retry, no log. A wrong client ID, a rotated secret, or a browser origin the project does not allow therefore stops analytics silently — nothing for an application to catch, nothing in the logs, and a dashboard that simply goes quiet. Found while wiring up a real project: server events were landing, browser events were 401ing from localhost, and the only way to see it was to read the response status off the wire.

Every other failure the SDK at least console.errors after its retries. 401 is the one that disappears.

The change

Both SDKs send through the api instance they expose publicly, so the providers replace its fetch with an equivalent implementation that reports the response status before returning the value the SDK expects.

new OpenPanelServerProvider({
  clientId: process.env.OPENPANEL_CLIENT_ID!,
  clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
  onDeliveryFailure: (failure) => {
    logger.warn("openpanel_delivery_failed", {
      attempts: failure.attempts,
      payloadType: failure.payloadType,
      reason: failure.reason,
      status: failure.status,
    });
  },
});
  • reason is unauthorized, server_error (still rejected once retries are exhausted) or network_error (never produced a response).
  • Without a handler the failure is logged, because the previous behaviour was no signal at all.
  • The failure carries the reason, status, attempt count, OpenPanel envelope type and ingestion URL — never event properties, matching the rest of trakoo's logging.
  • Delivery still resolves rather than throwing, so reporting a failure can never turn a tracked event into an application error. A throwing handler is swallowed too.

Why client.api is the seam

api is public in the shipped .d.ts; the Api class itself is not exported, so openPanelApiOf matches it structurally — one cast, in one place, behind runtime guards on fetch, baseUrl and headers. If a future SDK changes that shape, instrumentOpenPanelDelivery returns false, the SDK keeps delivering events unchanged, and only a debug log mentions it. The mocked-SDK tests already exercise that path.

The replacement mirrors the SDK transport exactly, so nothing else changes:

  • 200 and 202 are the only success statuses (a 204 still fails, as today).
  • 401 is never retried; other failures back off exponentially from the same maxRetries/initialRetryDelay read off the instance.
  • ...options is spread last, so send()'s keepalive: false for replay still wins.
  • Headers are resolved per request, so a later addHeader is picked up, and headers resolving to null are dropped.

Tests

test/openpanel-delivery.test.ts covers the unit behaviour (no retry on 401, retry-then-report on 5xx, network failures without an invented status, accepted responses returned untouched, the exact request shape, the unrecognized-transport guard, default logging, throwing handlers). Two of them go through the real SDKs with a stubbed fetch — server here, browser in openpanel-client-provider.web-sdk.test.ts — so an upstream shape change fails CI rather than silently disabling reporting.

Locally green: typecheck, lint, test (373), build, verify:package, docs validate --strict and docs build.

Notes

  • The real fix belongs upstream in @openpanel/sdk's post(). This is the consumer-side mitigation.
  • Not folded in: buildEventProperties spreads event properties first and then writes page, device, utm, category, sessionId, profileId and the __* keys over them, so an event declaring one of those names loses its value silently. Different blast radius, and it turns out the pattern is repo-wide rather than OpenPanel-specific — filed as Event properties are silently overwritten by provider context keys #35.

https://claude.ai/code/session_01MmqjXsp5iX3Upj27YiGiw1

OpenPanel's SDKs treat HTTP 401 as a non-error: their shared `Api.post`
returns `null` without throwing, retrying or logging. A wrong client ID, a
rotated secret or a browser origin the project does not allow therefore
stopped analytics silently, with nothing for an application to catch or a
log to show.

Both SDKs send through the `api` instance they expose publicly, so the
providers replace its `fetch` with an equivalent implementation that reports
the response status first. The replacement mirrors the SDK transport: 200
and 202 are the only success statuses, 401 is never retried, other failures
back off exponentially, and headers, body and request options are built the
same way. Delivery still resolves rather than throwing, so reporting a
failure cannot turn a tracked event into an application error.

Both providers accept `onDeliveryFailure`; without a handler the failure is
logged, because the previous behaviour was no signal at all. The reported
failure carries the reason, status, attempt count, OpenPanel envelope type
and ingestion URL, never event properties. When a client does not expose the
expected transport the SDK keeps delivering events unchanged and the skipped
instrumentation is only mentioned in debug logs.

Claude-Session: https://claude.ai/code/session_01MmqjXsp5iX3Upj27YiGiw1
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
trakoo Ready Ready Preview Sep 3, 2026 7:16am UTC

@multiplehats
multiplehats merged commit 683cfec into main Sep 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant