Skip to content

JITSU-88 fix(ingest): silent success on quota block + stream-id metric label#1413

Open
absorbb wants to merge 2 commits into
newjitsufrom
fix/ingest-quota-block-silent-response
Open

JITSU-88 fix(ingest): silent success on quota block + stream-id metric label#1413
absorbb wants to merge 2 commits into
newjitsufrom
fix/ingest-quota-block-silent-response

Conversation

@absorbb

@absorbb absorbb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Bulker side of JITSU-88 (PR 2 of the plan). Prepares ingest for the usage-quota block, which works by setting throttle=100 on a workspace. Must be deployed before the billing cron enables the 105% block.

1. Throttled events return a success response (not HTTP 402)

V1 blocking is silent — the client must see no ingestion error. Per the issue's V1 requirement ("stop sending event to destination and not have error on ingestion side") and the decision on the Linear thread.

sendToRotor now responds 200 {"ok":true} (or the tracking gif for /api/px/:tp) on throttle instead of 402 {"error":...}. The event is still preserved in backup (produced before the throttle check) and simply not delivered to destinations. The returned throttle marker is unchanged, so all internal accounting is preserved:

  • bulkerapp_handler_ingest{status="throttled"} metric
  • events-log status: "SKIPPED"
  • dead-letter copy (left as-is)

Batch handler: a throttled event counts toward okEvents instead of appending to errors, so a blocked batch returns ok: true. Classic handler: the trailing {"ok":true} write is now guarded by !c.Writer.Written() — gin appends body writes rather than guarding them, so this also fixes a pre-existing double-body write ({"error":...}{"ok":true}) on every sendToRotor error path there.

2. IngestHandlerRequests slug label → stream id

The slug label was filled with DefaultString(loc.Slug, loc.Domain) — ambiguously a slug or a domain. It now uses metricsId, which is the resolved stream.Stream.Id (falling back to UNKNOWN only for pre-resolution errors). Throttle rejections are always post-resolution, so blocked-event volume can be read per stream from bulkerapp_handler_ingest{status="throttled"} and mapped to a workspace — this is how the billing cron computes {blocked_count}.

⚠️ Deploy caveat: this changes the slug label across all ingest statuses (success + error too), not just throttled. Grafana panels / alerts keying on the old domain-style slug values need a look before this deploys.

Testing

go build + go vet clean. No automated test added — the ingest package has no HTTP-handler test harness (existing tests cover only pure helpers like batch dedup), so a response-level test would need scaffolding that doesn't exist. Behavior verified by tracing all five handlers (ingest/s2s, pixel, classic return clean single success responses; batch reports success in the aggregate; funcs has no throttle path).

Related

  • Billing core: jitsucom/jitsu-cloud-billing#25
  • Follow-ups: infra CronJob (dry-run first), quotaStatus in /api/billing/settings + console banners, legacy set-throttle removal.

🤖 Generated with Claude Code

…JITSU-88)

Two changes preparing ingest for the JITSU-88 usage-quota block, which
works by setting throttle=100 on a workspace.

1. Throttled events now return a success response instead of HTTP 402.
   V1 blocking must be silent — the client should see no ingestion error
   (events are still preserved in backup and simply not delivered to
   destinations). sendToRotor writes {"ok":true} (or the tracking gif for
   the pixel endpoint) and returns the throttle marker, which still drives
   the SKIPPED events-log status, the `throttled` metric and the
   dead-letter copy. In the batch handler a throttled event counts toward
   okEvents instead of failing the batch. The classic handler's trailing
   success write is now guarded by !Written() — gin appends body writes
   rather than guarding them, so this also fixes a pre-existing double-body
   write on every sendToRotor error path there.

2. IngestHandlerRequests now labels `slug` with metricsId (the resolved
   stream id) instead of DefaultString(loc.Slug, loc.Domain), which was
   ambiguously a slug or a domain. Throttled counts are always
   post-resolution, so blocked-event volume can now be read per stream from
   bulkerapp_handler_ingest{status="throttled"} and mapped to a workspace.
   NOTE: this changes the `slug` label across all ingest statuses (success
   and error too) — Grafana panels keying on the old domain values need a
   look before deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jitsu-code-review[bot]
jitsu-code-review Bot previously approved these changes Jul 20, 2026

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the ingest throttling and metrics-label changes across router handlers. One potential correctness regression noted inline: throttled non-pixel requests now return JSON {"ok":true} unconditionally, which can break clients expecting endpoint-specific 200 response bodies (for example plaintext/gif-style beacons or CORS preflight-sensitive integrations). Please confirm this intentional behavior across all ingest endpoints.

Comment thread bulker/ingest/router.go
if c.FullPath() == "/api/px/:tp" {
c.Data(http.StatusOK, "image/gif", appbase.EmptyGif)
} else {
c.JSON(http.StatusOK, gin.H{"ok": true})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Potential regression: this now returns {"ok":true} for every non-pixel throttled endpoint. Previously throttled responses were produced by ResponseError(...), which may have preserved endpoint-specific response shapes/headers. Could this break clients that parse expected 200 bodies for classic/batch/ingest variants? If silent-success is required, would preserving each handler’s normal success response format be safer?

@absorbb

absorbb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Added ERROR_ON_THROTTLE config var (default false).

  • false (default) — the silent-success behavior this PR introduces: a fully-throttled (quota-blocked) event is accepted with HTTP 200, preserved in backup, not delivered to destinations.
  • true — surfaces the block as a client-visible HTTP 402 (the pre-JITSU-88 behavior), for operators who want the rejection to show up in the client's own monitoring.

The flag gates both the single-event response (sendToRotor) and the batch aggregate (throttled events count as ok when silent, as errors when erroring). Internal accounting — SKIPPED events-log status, throttled metric, dead-letter copy, backup — is identical in both modes.

This gives us a per-deployment escape hatch without another code change if we decide 402 is preferable after watching the silent rollout.

jitsu-code-review[bot]
jitsu-code-review Bot previously approved these changes Jul 20, 2026

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the ingest throttling behavior and metrics-key changes across the touched handlers.

I found one potential correctness regression around response shape for throttled classic requests and left an inline comment with details.

Comment thread bulker/ingest/router.go
if c.FullPath() == "/api/px/:tp" {
c.Data(http.StatusOK, "image/gif", appbase.EmptyGif)
} else {
c.JSON(http.StatusOK, gin.H{"ok": true})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Potential bug: for non-pixel endpoints this throttle branch now always writes {"ok":true} when ERROR_ON_THROTTLE=false. In ClassicHandler, successful responses include destination lists (async, tag) when available, and some clients may rely on those fields. Returning a different success schema only for throttled requests can cause subtle client-side regressions. Could we preserve each endpoint’s normal success payload (or at least Classic’s shape) while still treating the event as silently skipped?

… response

Adds ERROR_ON_THROTTLE, default true — preserves the pre-JITSU-88 behavior
where a fully-throttled (quota-blocked) event is rejected with HTTP 402.
Merging this PR therefore changes no default behavior; the silent block from
the previous commit becomes opt-in (ERROR_ON_THROTTLE=false): the event is
accepted (HTTP 200) and preserved in backup but not delivered to destinations.

The flag gates both the single-event response (sendToRotor) and the batch
aggregate (throttled events are errors when erroring, ok when silent).
Internal accounting — SKIPPED events-log status, `throttled` metric,
dead-letter copy, backup — is identical in both modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@absorbb
absorbb force-pushed the fix/ingest-quota-block-silent-response branch from 66f114d to f9d0b4e Compare July 20, 2026 13:01
@absorbb

absorbb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Correction: ERROR_ON_THROTTLE now defaults to true (not false) — i.e. the pre-JITSU-88 behavior of rejecting quota-blocked events with HTTP 402 is the default, so merging this PR changes no production behavior. The silent-success block is opt-in via ERROR_ON_THROTTLE=false. Everything else in this PR (stream-id metric label, classic double-write guard) still applies unconditionally.

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the ingest throttling and metrics-label changes across , , and handler call sites in this PR range ().\n\nI focused on correctness, user-visible behavior, and security-sensitive paths (throttle branching, response writing, and metrics attribution).\n\nNo additional actionable issues found beyond the existing discussion threads.

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the ingest throttling and metrics-label changes across config.go, router.go, and handler call sites in this PR range (da3912b..f9d0b4e).

I focused on correctness, user-visible behavior, and security-sensitive paths (throttle branching, response writing, and metrics attribution).

No additional actionable issues found beyond the existing discussion threads.

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