The current state of the springbig platform, surface by surface, as of 16 August 2026. Every figure is a direct measurement against live production — 34.9 million web requests, 19 million background jobs, 369 deployed endpoints, 1,799 edge zones and the databases behind them. Where a trend matters, an earlier reading from 4 August is shown alongside.
The platform is stable and getting busier. What follows is not a list of outages — it is a set of places where the system is working exactly as configured, and the configuration is not what anyone intended.
| What works | Evidence (measured 16 August) |
|---|---|
| Server-side stability | 35,538 server errors across 34.9M requests — 0.102%, with no incident in the window |
| Latency | p50 30ms, p95 422ms, apdex 0.98 across 34.9M transactions |
| Job fleet | 19,071,497 executions over 3 days at 99.955% success |
| Ingestion | ~341,000 visits/day across 36 vendors; every vendor delivering on the last full day |
| Polling hygiene | Wasted polling against dead accounts is now zero — a concern at the prior measurement, resolved |
| Growth without incident | Throughput +7% and compute +30% between readings with no stability change |
Two revenue-assurance findings that no error dashboard can see. 31 of 233 merchants labelled messaging-only are running loyalty mechanics — Fire and Flower awarded 66.6M points in 90 days — because platform_type gates four call sites in the entire application, none of them on the API or the loyalty path. Separately, 79 billing organisations sent 26.8M messages in 90 days against $897k of collectible unpaid invoices — enterprise accounts billed off platform are excluded from that figure.
Observability remains configured to hide its own failures. New Relic records zero error events across 345,845 HTTP 500s. Rollbar discards RecordNotFound outright and files ActionView::Template::Error as a warning on the strength of a comment that is no longer true. The wallet project — a live consumer surface serving 1.98M requests a week — reports nothing at all.
The highest-volume failure is still unattributable, but the class around it is not. The transaction-date defect held at 23,112–27,550/day, unchanged across both measurements and still logged without merchant or POS identity. But duplicate-member errors — now the platform's largest error class at 24,968/day — are attributable today via enrollment_change_source, which had been assumed impossible.
The public API contract is narrower than a raw endpoint count suggests, and sharper within its real scope. The documentation was enumerated completely. pos and general are the two gateways public by design; across their 93 endpoints only 23 are documented — 24.7%. 35 undocumented endpoints mutate data — twelve delete records or move member balances, including DELETE /members/{} and PUT /members/{}/reset_balance. Every one of the 24 API reference pages declares a non-production host, with a live "Try It" explorer pointed at it.
One partner is generating 350,000 rejected requests a day, and is now named. A single API key — lmlp-leaflogix, issued 2022 and never rotated — accounts for 100% of missing-header rejections on the busiest POS endpoint, 29.3% of that partner's own calls to it. Seven other partners transact on the same endpoint with zero such failures. The traffic is rejected at the gateway, so it never reached an application metric and nobody reported it in over thirty days.
Two safety nets are not catching anything. A dead-letter queue holds 722 undelivered POS customer-sync messages that nothing has ever consumed, its oldest message pinned at the four-day retention cap — so failures are being destroyed on a rolling basis. All 87 queues, and all 38 dead-letter queues, sit below the fourteen-day maximum. And there is not a single CloudWatch alarm on any SQS queue, which is the complete explanation for why nobody knew. Separately, 31 alarms have received no data at all, 25 of them for between 165 and 1,274 days.
Webhooks are noisier in reputation than in fact, and blinder than expected. Firing is guarded at three levels and volume is proportionate to business activity — the apparent excess is POS location fanout, and 73% of the delivery lambda's traffic turned out to be inbound callbacks rather than outbound webhooks. But 0 of 816 endpoints can receive a change-set: the dirty state is discarded before the payload is built, so every receiver must blind-write the full record. That is the mechanism behind the known full-object clobber.
One recurring defect produced two fresh instances during the gap. Two audiences fail daily on date_trunc('1', …) — the exact misconfiguration diagnosed on 2026-08-08. The shape is still writable, so it recurs roughly every three to six days.
| # | Action | Why now |
|---|---|---|
| 1 | Raise the missing-header defect with Dutchie (key lmlp-leaflogix) | ~350,000 rejections/day, 29.3% of their calls to that endpoint, 100% of all such failures. Named partner, evidence captured, no engineering work needed on our side. |
| 2 | Enforce ConditionValueValidator on the audience write path | Third occurrence of one defect; two new instances between readings |
| 3 | Turn on New Relic error collection; re-scope the two Rollbar filters | Zero error events across 345,845 HTTP 500s — no alert can fire from either source today |
| 4 | Reconcile the 31 messaging-only accounts running loyalty, then wire require_platform! into the API | Reconcile first — enforcing today changes live behaviour for merchants with real programmes |
| 5 | Work the 79 billing orgs with collectible open invoices | $896,551 the platform should have collected; enterprise off-platform contracts already excluded |
| 6 | Add merchant_id and pos_type to the transaction-date rescue | The one error class that genuinely cannot be attributed; ~25,000/day |
| 7 | Merge observability-quick-fixes | Already written; restores per-worker metrics and removes ~92% of log spend |
| 8 | Add SQS alarms and raise DLQ retention to 14 days | Zero SQS alarms exist; 722 messages expiring unreviewed. Configuration, not code. |
| 9 | Document or gate the 35 undocumented mutating endpoints | Twelve delete records or move balances. Publishing them without first adding per-method authorisation would remove the only thing currently limiting access. |
| 10 | Add a changes key to webhook payloads | 0 of 816 endpoints can tell what changed — the root of the full-object clobber |
| 11 | Upload frontend source maps | Zero of the top 15 frontend errors are readable, and the backlog cannot be recovered retroactively |
| 12 | Resolve or disable cova location 26112 | Billing-active, still polled, no data since 2026-05-20 |
The strongest surface measured, and the one whose cost is most concentrated.
| Status | Requests (7d) | Share |
|---|---|---|
| 200 | 28,150,388 | 80.548% |
| 404 | 2,968,775 | 8.495% |
| 401 | 2,046,372 | 5.855% |
| 304 | 730,519 | 2.090% |
| 422 | 628,587 | 1.799% |
| 302 | 302,380 | 0.865% |
| 400 | 40,920 | 0.117% |
| 500 | 35,538 | 0.102% |
| 403 | 13,876 | 0.040% |
404 and 401 together are 14.35% of all traffic, and neither is a defect. The 404s are overwhelmingly the POS lookup-then-create protocol: a partner asks whether a member exists, is told no, and creates one. The 401s are dominated by two endpoints known to be broken since the August endpoint census — api/pos/v1/batches/create (829,971 in 7 days) and api/wallet/auth/v1/remember_me/authenticate (320,896) — plus routine unauthenticated probing.
| Endpoint | Requests | Avg | CPU hrs / 7d |
|---|---|---|---|
| api/pos/v1/members/index | 4,116,835 | 295.8ms | 338.3 |
| api/pos/v1/members/show | 7,487,489 | 35.3ms | 73.4 |
| api/pos/v1/visits/create | 1,646,093 | 97.5ms | 44.6 |
| api/native_app/v1/stashboard/show | 3,057,228 | 51.3ms | 43.6 |
| api/wallet/v1/members/show | 661,965 | 214.7ms | 39.5 |
| api/pos/v1/rewards/index | 3,069,181 | 46.1ms | 39.3 |
| api/pos/v1/offers/index | 2,530,461 | 44.4ms | 31.2 |
| api/pos/v1/members/update | 851,183 | 72.0ms | 17.0 |
The deployed gateway and the Rails application still disagree, and the gap is wide, and one long-standing assumption about it turns out to be false.
Of the 369 deployed endpoints, 305 proxy to Rails and 64 resolve to Lambda, SQS or mock integrations. Of those 305, 276 match a Rails route and 29 do not — they are deployed, reachable, and land on nothing.
| Gateway | Dead routes |
|---|---|
| brands | 10 |
| native_app | 8 |
| wallet | 4 |
| general | 3 |
| pos | 3 |
| callbacks | 1 |
DELETE /members/{} and PUT /members/{}/reset_balance both remain live on the pos gateway as proxies to Rails, and both have matching Rails routes. Both require an API key. That key is authentication, not authorisation: it establishes that the caller is a valid partner and then grants the whole stage. All 93 endpoints on the two public gateways carry authType = NONE — no Lambda authoriser, no IAM, no per-method scope — and none of the six usage plans throttles per method. A destructive endpoint is configured identically to a read endpoint; the only thing separating them is knowing the path.
The published documentation was enumerated completely — 48 pages, all HTTP 200, via the readme.io site manifest and its embedded OpenAPI fragments. Every claim below comes from the published source text. The headline earlier estimate is overturned — not because it was too harsh, but because it was too generous.
The docs were enumerated completely rather than sampled: readme.io publishes a machine-readable site manifest at llms.txt, and each reference page embeds a full OpenAPI 3.1 fragment. All 48 pages returned HTTP 200 — no login wall, no rate limiting. One page (docs/template-api) is absent from the manifest and reachable only by following an inbound link.
| Declared host | References | Pages |
|---|---|---|
| gamma.api.springbig.technology | 26 | 24 |
| production.api.springbig.technology | 10 | 8 — all integration guides, no reference pages |
| {environment}.api.springbig.technology | 7 | 4 |
| api.springbig.technology (deprecated form) | 1 | 1 |
pos and general are the externally documented gateways. The other ten are consumed by internal services — native app, wallet, brands, callbacks, tablet, join, reports, netsuite, analytics and shortlinks — and are either active internal surfaces or superseded. They are not expected to carry public documentation, and their absence from readme.io is correct, not a gap.
Matching was mechanical — lowercase, strip the version prefix, treat {param} names as wildcards, exclude OPTIONS. Zero of the 23 matches required judgement.
| Measure | Count |
|---|---|
| Documented operations (pos + general) | 27 |
| Deployed endpoints (pos + general, excl. OPTIONS) | 93 |
| Matched | 23 |
| Documented but not deployed | 4 — all typos with working counterparts |
| Deployed but undocumented | 70 (pos 32, general 38) |
| Documentation coverage | 24.7% |
The four documented-but-not-deployed cases are all near-miss typos rather than missing functionality: new-ten_dlc_submissions (editing artifact), /ten_dlc_submissions/status (guide omits {submission_id}), /attachment versus the deployed /attachments, and the malformed /{pos_user}/rewards. Each has a working deployed counterpart at a slightly different path, so the functional documentation-only gap is zero.
35 of the 70 undocumented endpoints are POST, PUT, PATCH or DELETE. Twelve of those touch member balances or delete records outright:
| Method | Path | Risk |
|---|---|---|
| DELETE | /v1/members/{pos_user} | Destructive — member deletion |
| DELETE | /v1/external_group_segments/{id} | Destructive — segment deletion |
| PUT | /v1/members/{pos_user}/reset_balance | Balance-affecting |
| PUT | /v1/members/{pos_user} | Balance-affecting via full-object clobber |
| POST | /v1/members/{}/cash_back/redeem | Balance-affecting |
| POST | /v1/members/{}/cash_back/{}/refund | Financial reversal |
| POST | /v1/members/{}/rewards/{}/redeem_points | Point deduction |
| PATCH | /v1/members/{}/reward_redemptions/{} | Mutates a settled redemption |
| PATCH | /v1/visits/{pos_id} | Mutates a settled transaction |
| POST | /v1/members/{}/send_message | Sends real SMS — cost and compliance bearing |
Measured over 30 days from the POS gateway's own access log (/aws/apigateway/pos-production), by resourcePath:
| Endpoint | Calls (30d) | Documented? |
|---|---|---|
| POST /v1/visits | 7,566,312 | guide only |
| PUT /v1/members/{pos_user} | 3,595,269 | no |
| POST /v1/members | 1,649,282 | yes |
| POST /v1/members/{}/rewards/{}/redeem | 484,196 | no |
| PATCH /v1/members/{}/reward_redemptions/{} | 76,039 | no |
| POST /v1/members/{}/offers/{}/redeem | 61,584 | no |
| POST /v1/members/{}/rewards/{}/redeem_points | 3,046 | no |
| PATCH /v1/visits/{pos_id} | 7 | no |
| DELETE /v1/members/{pos_user} | 1 | no |
The redemption endpoints are not edge cases either. Over the same window POST .../rewards/{reward_id}/redeem succeeded 482,691 times against 1,234 validation failures and 240 rejections — core loyalty behaviour running entirely on undocumented paths.
| Log group | resourcePath | Actual path |
|---|---|---|
| /aws/apigateway/pos-production | /v1/members/{pos_user} | /api/pos/v1/members/{pos_user} |
| general gateway | /v1/members/{member_id} | /api/general/v1/members/{member_id} |
| — not gateway-exposed — | — | /api/v1/members (internal, routes.rb:64) |
All 48 pages were searched for reset_balance, DELETE, redeem, deduct and adjust:
| Endpoint | Documented? | Problem |
|---|---|---|
| POST /pos/v1/members | yes | Response body accurate; declares 2 status codes, returns 6, including 20.5% HTTP 500 |
| PUT /pos/v1/members/{pos_user} | no | Entirely undocumented |
| POST /pos/v1/visits | partial | Appears only in the Shopify Flow guide — no reference page, no schema |
The documented POST /pos/v1/members declares only 200 and 422 responses, and its 200 example returns a fully-populated member. That example is accurate: measured over 7 days, successful responses average 1,878 bytes (minimum 1,316) and integration latency is 96% of total response latency, so the Lambda returns synchronously with a real body. The read-after-write expectation is safe. What is not accurate is the response-code list — see below.
The specification was compared against 30 days of POS gateway access logs — 98,961,673 requests, full-window aggregation, no sampling.
| Endpoint | Declared | Observed |
|---|---|---|
| POST /v1/members | 200, 422 | 200, 500, 401, 400, 403, 422, 404 |
| GET /v1/members | 200, 404 | 200, 404, 401, 400, 500, 403 |
| GET /v1/rewards | 200 only | 200, 401, 500, 422, 403, 400 |
| GET /v1/members/{}/offers | 200, 404 | 200, 404, 401, 500 |
| Measure | Value |
|---|---|
| Endpoint | GET /v1/members/{pos_user} |
| Error | Missing required request parameters: [AUTH-TOKEN] |
| Volume (30 days) | 10,494,028 |
| Daily rate | ~350,000 (range 301,259–435,354) |
| Share of that endpoint | 24.4% |
| Share of all POS traffic | 10.6% |
| Distinct source IPs | 417 |
These are rejected at the gateway before reaching Rails, so they consume no application capacity — but they are billed, logged, and invisible to every application-side metric.
A complete failing request, verbatim from the execution log:
API Key ID: wd6n3j71og
HTTP Method: GET, Resource Path: /pos/v1/members/38685148
Method request headers: {x-datadog-sampling-priority=1, CloudFront-Viewer-Country=US,
x-datadog-origin=rum, CloudFront-Forwarded-Proto=https, User-Agent=Amazon CloudFront, ...}
Request parameter validation failed. Missing parameters: [AUTH-TOKEN]
Method completed with status: 400
Usage Plan check succeeded for API Key **********************************mmcaK9
| Key | Created | Plan |
|---|---|---|
| lmlp-leaflogix — the failing one | 2022-04-08 | pos-partner-plan |
| lmlp-dutchie | 2022-04-08 | pos-partner-plan |
| new-dutchie-pos | 2023-01-11 | pos-partner-plan |
| dutchie-ecom | 2025-03-13 | pos-partner-plan |
| dutchiepos-sms | 2025-04-18 | pos-partner-message-plan |
| Plan | Rate/s | Burst |
|---|---|---|
| pos-partner-message-plan | 300 | 400 |
| pos-partner-plan | 500 | 500 |
| pos-partner-high-limit-plan | 5,000 | 800 |
The documented "300 requests per second, burst 400" matches exactly one of the three POS plans, presented as though it were universal — a 16.7× spread between the slowest and fastest partner tier, undisclosed. A second page states 10,000 requests per minute, which matches no plan at all; the only quota in the estate is netsuite's 10,000 per day. Zero 429s occurred in 30 days across 99 million requests, so throttling is configured and never fires.
The reference specs uniformly declare AUTH-TOKEN in uppercase. The Shopify guide instructs lowercase auth-token and adds: "It is very important that the headers have the exact syntax." Across all pages the split is 50 uppercase to 16 lowercase. HTTP headers are case-insensitive, so this is probably harmless in practice — but the documentation asserts a strict requirement that contradicts its own reference section.
Two further inconsistencies: the Authorization guide's example shows AUTH-TOKEN: <partner_api_key> while the surrounding prose defines AUTH-TOKEN as the merchant token; and the published rate limit is 300 requests/second on one page and 10,000 per minute (167/second) on another. Which the gateway actually enforces cannot be determined from the documentation.
Twelve APIs, unchanged in a year of drift, still almost entirely uninstrumented — but the traffic they reject has fallen by nearly half.
| API | Endpoints | Access log | Metrics |
|---|---|---|---|
| brands | 67 | no | off |
| native_app | 63 | no | off |
| wallet | 52 | yes | off |
| general | 52 | no | off |
| callbacks | 46 | no | off |
| pos | 41 | yes | off |
| tablet | 15 | no | off |
| join | 13 | no | off |
| reports | 12 | no | off |
| netsuite | 4 | no | off |
| analytics | 2 | no | off |
| shortlinks | 2 | no | off |
The identifier map is unchanged since early August — no API added, none removed — and the orphaned h2dn0titad log group still exists with no API behind it. Only 2 of 12 have access logging, and metricsEnabled is false on all 12, so no per-route metric or alarm exists anywhere in the estate. That is unchanged.
| API | Measured (4h) | Extrapolated/day | 04 Aug |
|---|---|---|---|
| pos — missing AUTH-TOKEN | 98,056 | ~588,000 | 337,000 |
| analytics — missing API key | 52,533 | ~315,000 | 316,000 |
| general — schema validation | 2,425 | ~14,600 | 8,000 |
| callbacks — timeouts | 0 | ~0 | 447 |
| Total | 153,014 | ~918,000 | 1,640,000 |
Analytics rejections are entirely POST /v1/native_app/events with an API key not associated with a usage plan. The pos rejections are all Missing parameters: [AUTH-TOKEN]. Callbacks has effectively stopped rejecting — two rejections in a full 24-hour window, against 447 a day when last measured.
Three of the 41 pos endpoints bypass the monolith entirely:
| Method | Path | Integration |
|---|---|---|
| POST | /v1/members | Lambda — webhooks_delivery_service |
| PUT | /v1/members/{pos_user} | Lambda — webhooks_delivery_service |
| POST | /v1/visits | Lambda — webhooks_delivery_service |
Unchanged, along with the two general endpoints that resolve to SQS (auto_campaigns/trigger and message_template_logs). A further 17 callbacks endpoints route to the same Lambda and 12 reports endpoints to an Athena handler.
The job fleet is reliable. The measurement of it needed correcting, and the corrected figures are below.
| Environment | Starts | Dones | Fails | ERROR lines |
|---|---|---|---|---|
| -secondary | 17,569,012 | 17,566,128 | 2,890 | 186,498 |
| -v2 | 1,502,485 | 1,496,842 | 5,640 | 4,608 |
| Total (3 days) | 19,071,497 | 19,062,970 | 8,530 | 191,106 |
Success rate 99.955%, roughly 6.36M job starts a day, and the books balance (start − done − fail = −3). -secondary carries 92% of all jobs, which confirms the inverted environment naming: the environment that serves almost no web traffic runs nearly all the background work.
Fail lines read, verbatim:
I, [2026-08-16T21:58:50.223153 #51824] INFO -- : fail
No class, no JID, no elapsed time — only a PID. Per-worker execution counts, success rates and durations remain unobtainable from this source. The fix is written and waiting on the observability-quick-fixes branch, which sets a JSON formatter and level: :info; the latter also removes the ~92% DEBUG-SQL volume that makes a 30-day retention unaffordable today.
The platform's most persistent error is unchanged. But the error class around it turns out to be attributable today, which narrows the biggest standing recommendation considerably.
| Window | Count | Basis |
|---|---|---|
| 14 August (full day, UTC) | 27,550 | measured |
| 15 August (full day, UTC) | 23,112 | measured |
| 04 August | 26,845/day | — |
| May 2026 audit | 47,901/day | — |
Still logged as a bare string with no merchant, no location and no POS type:
[merchant_location] Validation failed: Transaction date can not be greater than created date
The rescue site (pos_merchant_location_concern.rb, ~line 175) logs only the exception object and is annotated # do nothing. The top standing recommendation — add merchant_id and pos_type here — remains unimplemented.
| Error (full day, 15 August) | Now | 04 Aug |
|---|---|---|
| Email has already been taken | 24,968 | 7,709 |
| Transaction date can not be greater than created date | 23,112 | 26,845 |
| Mobile subscriber has already been taken | 6,837 | 3,800 |
| Pos has already been taken | 872 | — |
| Phone number is invalid | 14 | — |
Duplicate-email failures have tripled and are now the platform's largest single error class.
| Source | Count (4h, measured) |
|---|---|
| treez | 1,018 |
| hifyre | 940 |
| leaflogix | 433 |
| cova | 176 |
| janepos | 105 |
| flowhub_maui | 94 |
| blaze | 63 |
| shopify | 52 |
| web_wallet | 37 |
| zapier | 23 |
No code change is required to produce this. Only the transaction-date rescue site genuinely lacks attribution — which makes that one fix both smaller and more clearly worth doing.
The code census was run as an AST walk rather than a text search, so the counts are exact. What is new is the discovery that some of these rescues do not merely lose a log line — they can persist wrong data.
Re-run as a Ruby Ripper.sexp AST walk over all 2,350 .rb files in app/ and lib/, rather than a text search. Zero parse failures.
| Level | 04 Aug | Now | |
|---|---|---|---|
| info | 288 | 288 | confirmed |
| error | 273 | 273 | confirmed |
| warn | 56 | 56 | confirmed |
| debug | 16 | 16 | confirmed |
| tagged | 10 | 10 | confirmed |
| Total | 643 | 643 | confirmed |
A naive text search returns 644. The single difference is a commented-out logger call in headset_integration/location_service.rb:43, which the AST correctly excludes. Two directory-level movements since 4 August are both explained by git history: one worker logging call was removed in the Lightspeed token-refresh refactor, and one was added in the new enroll/home_controller.rb.
| Metric | 04 Aug | Now |
|---|---|---|
| Log calls inside a rescue body | 257 | 258 |
| Of those, swallowing | 201 | 208 |
| Distinct swallowing rescue blocks that log | — | 170 |
| Directory | 04 Aug | Now |
|---|---|---|
| app/services | 73 | 73 |
| app/models | 50 | 51 |
| app/workers | 40 | 45 |
| app/controllers | 32 | 33 |
| app/helpers | 4 | 4 |
| app/policies | 2 | 2 |
When a rescue swallows inside a Sidekiq perform body, the job records as succeeded — no retry, no dead-letter queue, no alert. The subset can be counted precisely. 42 of the 208 swallowing sites sit directly inside a perform method, across 33 worker files, with zero ambiguous attributions.
| File | Swallowing sites |
|---|---|
| app/models/member_profile.rb | 12 |
| member_import/mjfreeway_email_processor_service.rb | 11 |
| app/models/external/optimove.rb | 8 |
| services/ai/unified_chat_service.rb | 7 |
| services/shopify_service.rb | 7 |
| api/wallet/v1/members_controller.rb | 6 |
| concerns/pos_merchant_location_concern.rb | 6 |
The site behind the platform's most persistent error, verbatim:
begin
visit.save!
if visit.persisted? && pos.try(:visit_details_included?, o)
visit.visit_details = pos.create_visit_details(visit, o.dig("line_items"))
end
rescue ActiveRecord::RecordInvalid => e
logger.tagged("merchant_location") do
logger.info "trying to save a visit"
logger.error e
end
# do nothing
# its just sidekiq trying to save the same visit in multiple threads
It still swallows, and it logs no merchant id, no location, no pos type and no order identifier — only a literal tag and the exception. That is why ~25,000 failures a day cannot be attributed to anyone.
Twelve of the 208 sites sit in app/models/member_profile.rb. They look like safe retry loops and are not — the retry is guarded, so when the counter is exhausted control falls off the end of the rescue and the exception is absorbed:
member_profile.save!
rescue ActiveRecord::StatementInvalid => e
logger.tagged('member_profile') do
logger.error 'Trying to create a member_profile'
logger.error e
end
if (retries -= 1).positive?
sleep rand(0.1..0.8) # to avoid db lock due to sidekiq race conditions
retry
end
save! is the bang form, chosen precisely so it raises on failure. On the terminal path that raise is absorbed, the method returns normally, and the caller cannot distinguish "profile created" from "profile never persisted." The condition this code was written for — lock contention between concurrent Sidekiq jobs, per its own comment — is exactly the condition under which every retry fails and the loss is silent.
Strong across the board, with one location that has quietly gone dark.
Roughly 341,000 visits a day across 36 active vendors. Every vendor with meaningful volume delivered on the last full day measured; none stopped.
| POS | Visits/day | Week over week | Merchants |
|---|---|---|---|
| dutchiepos | 87,189 | −0.7% | 280 |
| cova | 45,508 | −2.1% | 75 |
| hifyre | 38,475 | +28.3% | 17 |
| posabit | 20,239 | −1.8% | 35 |
| treez | 18,594 | +0.4% | 48 |
| flowhub_maui | 14,553 | −4.3% | 42 |
| blaze | 13,183 | −1.1% | 42 |
| lightspeed | 9,982 | +0.7% | 20 |
| greenline | 7,990 | −0.1% | 30 |
cova location 26112 (Alpha Cannabis, merchant 11637) logged 17,795 failed 401s in seven days while visits landed normally, and was deliberately excluded from the 8 August cleanup on that basis.
Re-measured: zero visits since 2026-05-20, roughly three months, while the location remains enabled and actively scheduled for polling. The errors that were correctly dismissed now sit alongside no ingestion at all.
lost 3,863 · active 732 · pending 158 · suspended 56 · onboarding 12. Stable. Lost outnumbers active better than five to one, so any platform-wide error count that does not filter on merchant status is dominated by dead accounts.
129 billing-active merchants have sent zero visits in 60 days (245 silent, less the 116 that never transacted); roughly 30 have been silent for over two years. Four dormant accounts flagged earlier have not recovered: merchants 10370 (last visit 2024-12-16), 13977 (2025-02-01), 12143 (2026-01-09) and 17351 (2026-04-01).
One cluster is worth a targeted check: four dutchiepos merchants — The Vault Spokane (2066), Silvana (2358), Lake Stevens (2853) and Herbn Elements (3650) — went silent on exactly 2026-05-21, each retaining one enabled location. dutchiepos overall is healthy, so this is per-account rather than a vendor problem.
This is answerable for one of the two largest error classes and not the other. One of the two can now be answered — and the answer points at us more than at them.
The question matters commercially: when a POS integration produces hundreds of thousands of errors a week, the response differs entirely depending on whether the partner is sending bad data or springbig is mishandling good data. That reading concluded honestly that an honest split was not computable. That is now half true.
Duplicate-member failures — the platform's largest error class at 24,968 a day — carry opt_event_data.enrollment_change_source on the retry line. Over a four-hour window they resolve cleanly:
| Source | Failures (4h) | Share |
|---|---|---|
| treez | 1,018 | 33.6% |
| hifyre | 940 | 31.0% |
| leaflogix | 433 | 14.3% |
| cova | 176 | 5.8% |
| janepos | 105 | 3.5% |
| flowhub_maui | 94 | 3.1% |
| blaze | 63 | 2.1% |
| shopify | 52 | 1.7% |
| web_wallet | 37 | 1.2% |
| zapier | 23 | 0.8% |
| posabit | 22 | 0.7% |
| dutchie_ecomm | 16 | 0.5% |
A duplicate-member error means the platform tried to create a member who already exists. That can be the partner re-sending, or it can be springbig's own matching logic failing to recognise an existing member. The member-identity code path is the one carrying twelve swallowing rescues that null out failed fields and retry — so a member record whose email or mobile was silently dropped on a previous write will not match on the next one, and will present as a duplicate.
The transaction-date defect — roughly 25,000 failures a day — remains genuinely blind. Its rescue site logs a literal tag and an exception object with no merchant, location, POS type or order identifier. It is now the only major error class in that state, which makes the instrumentation recommendation both smaller in scope and harder to defer.
Every transaction is instrumented. No error is. This is a settings problem, not a tooling problem, and it is the cheapest item on the list.
| Metric (7 days) | 04 Aug | Now | |
|---|---|---|---|
| Transactions | 52,226,146 | 55,670,502 | +6.6% |
| Background jobs | 16,358,570 | 17,503,582 | +7.0% |
| Background hours | 735 | 959 | +30.5% |
| Web hours | 794 | 1,033 | +30.2% |
| TransactionError events | 0 | 0 | unchanged |
Compute grew about 30% while request volume grew about 7%. Work per request is rising faster than traffic — not yet a problem, but the trend is worth watching, and api/pos/v1/members/index at 338 hours a week is where it concentrates.
Rollbar's active item count more than doubled in twelve days. That is not necessarily more breakage — but nobody is triaging it, and the highest-volume items carry no stack trace.
| Metric | 04 Aug | Now | |
|---|---|---|---|
| rails active items | 69 | 166 | +141% |
| frontend active items | 91 | 115 | +26% |
| frontend total occurrences | 265,777 | 266,521 | +744 |
| frontend oldest item | 2,028d | 2,040d | confirmed |
| frontend items > 1yr / > 3yr | 12 / 11 | 19 / 16 | worse |
| wallet items | 0 | 0 | still silent |
84 of the 166 rails items (51%) are new since 4 August, and 118 of 166 (71%) fired within the last three days. This is a live surface, not a stale backlog. The new items are numerous but individually low-volume — a broad front rather than one runaway error.
The wallet project's silence is now proven rather than assumed: its token authenticates normally and returns zero for active, resolved and muted alike. A live consumer surface serving 1.98M requests a week reports nothing at all. The frontend project shows 0 resolved and 0 muted against 115 active — nobody is working it.
config/initializers/rollbar.rb discards or downgrades whole exception classes:
'ActiveRecord::RecordNotFound' => 'ignore', 'ActionController::RoutingError' => 'ignore', 'ActionView::Template::Error' => 'warning', # this is just CS people / ActiveAdmin
Both rules are individually defensible and collectively blinding. The RecordNotFound rule cannot distinguish a bot probing a URL from a member whose link stopped working — both raise the same class and both are dropped. The ActionView::Template::Error comment is an assumption that has outlived its context: the same class now fires on customer-facing enrol pages and on the API. The Reward 48176 deletion produced 194 occurrences of it between 13 and 14 August, all filed as warnings, resolving to app/views/api/web/v1/extension_rewards/_reward.jbuilder:1.
The sharpest earlier claim on this surface is confirmed emphatically. Two earlier counts are not reproducible from the underlying data and are withdrawn. And one dataset previously recorded as unavailable turns out to hold 410 million rows.
The top 15 frontend items account for 264,516 occurrences — 99.2% of all active frontend volume. One instance was fetched from each and every frame examined.
| Verdict | Items | Occurrences |
|---|---|---|
| Symbolicated — real file and function | 0 / 15 | 0 |
| Minified only | 12 / 15 | 195,694 |
| No usable trace at all | 3 / 15 | 68,822 |
Every frame resolves to a content-hashed bundle and a mangled identifier:
assets/index-mm3f9i1-.js :28:1996 [XMLHttpRequest.S] assets/index.afe282a1.js :16:1994 [RB] assets/index-BjBsRhPE.js :14:1401 [Y4]
Three independent signals of source-map resolution were checked across all frames — .vue/.ts extensions, webpack:/ or /src/ paths, and Rollbar's code field, which populates only when a map resolves. Zero hits on all three. Rollbar is receiving raw minified frames and resolving nothing.
| Class | Items | Occurrences | Share |
|---|---|---|---|
| Genuine application errors | 112 | 264,319 | 99.2% |
| Browser noise (ResizeObserver etc.) | 3 | 2,202 | 0.8% |
| Third-party scripts | 0 | 0 | 0.0% |
| Browser extensions | 0 | 0 | 0.0% |
Classification was done on stack-frame filenames rather than titles — extension protocol prefixes and known third-party CDN hosts — so it does not depend on error text. Of the top 45 items by volume, 35 originate on springbig.io itself and none on an extension or a third party. The inbox is almost entirely first-party signal that cannot be read.
| Error family | Items | Occurrences | Share |
|---|---|---|---|
| HTTP 401 from the API (axios) | 2 | 178,423 | 66.9% |
| Unhandled rejection with no reason | 1 | 66,788 | 25.1% |
| HTTP 404 from the API | 2 | 8,381 | 3.1% |
| vue-query used outside setup() | 40 | 4,868 | 1.8% |
| HTTP 422 from the API | 1 | 3,182 | 1.2% |
Figures of 198 mobile app projects and 318 cloud projects have circulated internally. Neither is reproducible from the underlying data.
| Candidate measure | Value |
|---|---|
| merchant_organizations with a native app name | 1,909 of 1,910 |
| Distinct native app names | 1,877 |
| Distinct native_app_firebase_project values | 272 |
| Distinct app names in telemetry (all time / 30d) | 194 / 124 |
One related figure is reconciled rather than disputed: 568 active non-demo merchants sit under an organisation with a native app; the 594 quoted elsewhere in this session is the same measure including 26 demo accounts.
Mobile telemetry has been described internally as not retained anywhere queryable. It is: springbig_production_logs.native_app_events holds 410,602,054 rows spanning January 2023 to today, live. It appears in Postgres as a foreign table and is unreadable there, but queries directly against Redshift.
| Measure | Value |
|---|---|
| Total rows | 410,602,054 |
| Events, last 7 days | 148,457 |
| Distinct devices, last 7 days | 7,324 |
| Distinct devices, all time | 1,129,437 |
| Distinct event names | 82 |
The wallet Rollbar project is enabled, its token is valid, and it returns zero items in every status — active, resolved, muted and suspended alike. It has eight registered environments including production, and Rollbar creates an environment only when events arrive, so it reported at some point and stopped.
A full sweep of all 1,799 zones — not a sample — gives the first complete picture of edge traffic. Nearly a quarter of it is a 4xx, and the plan tier prevents us from saying why.
| Class | Requests (7d) | Share |
|---|---|---|
| 2xx | 10,980,751 | 60.15% |
| 3xx | 2,601,639 | 14.25% |
| 4xx | 4,195,308 | 22.98% |
| 5xx | 477,539 | 2.62% |
The 4xx figure is dominated by 3,653,786 403s — 20.02% of all edge traffic. That should not be read as 3.65 million security events: a 403 at the edge is frequently a member-not-found rather than a genuine authorization failure. Distinguishing the two requires firewall event data, which is denied on the Free plan — re-tested at both zone and account level, still code=authz.
| Metric | 04 Aug | Now |
|---|---|---|
| Zones | 1,860 | 1,799 |
| Zones with traffic | 1,858 | 1,798 |
| Cache hit rate | 3.31% | 4.15% |
| Worker invocations (7d) | 15,996,051 | 12,221,543 |
| Firewall / bot / origin latency | not authorized | not authorized |
Cache remains effectively unused at 4.15% of requests, against a 59.3% byte hit rate — meaning a few large static assets cache well while nearly all dynamic and API traffic does not. Workers remain the well-instrumented layer at a 0.015% error rate, with two exceptions: wallet-3-testing-amplify fails 100% of its 85 invocations and wallet-3-testing-api 69.6% of 125. Volume is negligible, so these read as broken test workers rather than customer impact — but they fail silently.
Several low-volume zones fail on nearly every request and nobody is watching them: springbig.ai (99.3% 4xx), sbtest101.com (99.4% 5xx), stash-board.com (88.3% 5xx), springbig-wallet-alpha.click (72.1% 5xx).
The estate is healthy on invocations. What matters here is quieter: a large fraction of the alarm estate is not protecting anything, and one whole service has no alarms at all.
| Metric | 04 Aug | Now |
|---|---|---|
| Lambda functions | — | 49 |
| /aws/lambda/* log groups | 51 | 56 |
| Active (≥1 invocation in 7 days) | 28 | 37 |
| Silent | 23 | 12 |
| Orphan log groups (function deleted) | — | 7 |
| Function | ERROR lines / invocation | Invocations (24h) |
|---|---|---|
| analytics_process_incoming_events | 789% | 1,782 |
| web_request_runner | 215% | 67,663 |
| messaging_send_push | 178% | 1,757 |
| quickbooks_online | 61% | 723 |
| messaging_send_sms | 52% | 6,863 |
| pos_response_runner | 48% | 38,564 |
| webhooks_delivery_service | 27% | 1,029,485 |
Ratios exceed 100% because batch handlers emit many error lines per invocation — this is a lines-per-invocation measure, not a failure rate, and it is not comparable to the earlier 58.7%. analytics_process_incoming_events remains rank one with the identical error string quoted earlier.
| State | Count |
|---|---|
| OK | 93 |
| ALARM | 1 |
| INSUFFICIENT_DATA | 31 |
| Alarms with no action configured | 24 |
| Total metric alarms | 125 |
The structural point — CloudWatch notifies on state transition, so a stuck ALARM is invisible — remains architecturally true. But the specific instance it cited is not reproducible today: only one alarm is in ALARM and it tripped eleven minutes before measurement.
Only 2 of 49 functions run a deprecated runtime (both python3.9, both silent CDK helpers), and seven have been unmodified for over a year — including test_lambda, a test function deployed to production, silent for 370 days and still provisioned.
The "one repo hosts four lambdas" finding is confirmed: the pos-request-service repository deploys pos_request_runner, pos_cache_runner, pos_response_runner and pos_service_request as four separate CloudFormation stacks. Nothing in the function list reveals this — only the stack-name prefix does.
This was called "a delete timer wearing a safety-net label" and found one instance. It is not one instance. It is every queue in the account.
| Retention | Queues | Of which DLQ |
|---|---|---|
| 1 day | 19 | 0 |
| 4 days | 68 | 38 |
| 14 days (the AWS maximum) | 0 | 0 |
Every dead-letter queue in this account is a four-day delete timer. Not one queue uses the maximum retention, and the nineteen send-request queues at one day are tighter still.
| Queue | Messages | Retention | Oldest message |
|---|---|---|---|
| pos_customer_sync_payloads_dlq | 722 | 4 days | 4.00 days — at the cap |
| pos_order_requests_dlq.fifo | 20 | 4 days | 2.78 days |
Fourteen days of metrics on the larger queue show NumberOfMessagesReceived = 0 and NumberOfMessagesDeleted = 0 every single day — nothing has ever consumed it. Yet its depth oscillates between 571 and 722 while the oldest message sits pinned at exactly the four-day retention cap.
The "dead queue still provisioned" finding is confirmed literally: dead_queue exists with zero messages, alongside shoryuken_legacy_jobs and its DLQ.
The registration chain is in good shape, and the gap that exists is specific and actionable.
| Layer | Active | Inactive | Total |
|---|---|---|---|
| Brands | 740 | 172 | 912 |
| Campaigns | 753 | 326 | 1,079 |
Of 652 active non-demo merchants, 499 are flagged ten_dlc_enabled and expected to send SMS. Tracing the full brand → campaign → number chain for those 499:
| Gap | Merchants |
|---|---|
| No brand at all | 0 |
| Brand present but inactive | 0 |
| No active campaign | 2 |
| No active phone number | 36 |
| Any gap in the chain | 36 (7.2%) |
464 of 499 hold the complete chain. The 36 that do not have a valid brand and campaign and are missing only the number — a single, specific, fixable link rather than a registration failure.
| Merchant status | Active numbers | Merchants |
|---|---|---|
| suspended | 1,769 | 41 |
| pending | 588 | 12 |
| lost | 341 | 7 |
| onboarding | 294 | 6 |
| demo accounts | 297 | 9 |
| Total on non-active accounts | 3,289 | 68 |
The 341 numbers held by seven lost merchants are the clearest reclaim candidates — a departed account holding live 10DLC numbers is both a recurring cost and unnecessary carrier-registration surface.
Data-integrity notes from the same pass: 39 brands are orphaned (no merchant points at them), 2 active campaigns hang off an inactive brand, and all 652 active merchants carry a non-null legacy dlc_campaigns JSON column running parallel to the normalised tables — a possible dual source of truth that was not audited.
Two findings that belong together. Both describe a commercial arrangement and a running system that disagree, and neither appears on any monitoring surface — because in both cases every request succeeds.
Of 233 active, non-demo merchants labelled platform_type = messaging_only, 31 are running loyalty mechanics in the last 90 days.
| Signal (90 days) | Merchants |
|---|---|
| Awarding points (> 0) | 17 |
| 15 or more redemptions | 4 |
| 15 or more reward grants issued | 16 |
| Any of the above | 31 |
| Merchant | Points (90d) | Redemptions | Grants |
|---|---|---|---|
| Fire and Flower | 66,595,545 | 0 | 0 |
| Bud Supply Group | 5,942,818 | 0 | 0 |
| Lucid | 4,976,389 | 0 | 0 |
| Prairie Records | 2,951,955 | 0 | 0 |
| Garden Variety | 2,550,491 | 0 | 0 |
| Food 4 Thought | 195,010 | 1 | 19,501 |
| Electric Ave | 112,800 | 282 | 7,960 |
| High Ties | 0 | 1 | 908,706 |
| Natural Remedies | 0 | 19 | 35,614 |
| American Cannabis | 0 | 15 | 15,841 |
These are not recent reclassifications. Read against PaperTrail — up to 2,393 versions per merchant — every one has held messaging_only with loyalty = true for its entire recorded history. One merchant, High Ties, did move from loyalty_and_messaging on 2026-03-18, after which its points programme genuinely stopped (redemptions fell from 46,296 to 1) while its offers programme continued. 231 of the 233 messaging-only merchants carry loyalty = true.
The capability boundary exists in code and is almost entirely unwired. require_platform! is defined twice and called four times, all in two legacy server-rendered controllers. The api/web/v1 concern defines its own copy that nothing calls.
| Path | Platform-gated? |
|---|---|
| rewards_controller / merchants_controller (legacy) | yes — 4 call sites |
| api/web/v1/* — the dashboard API | defined, no callers |
| Visit ingestion and point accrual (visit.rb:413) | checks merchant.loyalty only |
| Grant issuance and redemption | ungated |
Measured at billing-organisation level over twelve months, excluding zero-amount invoices:
| Cohort | Orgs | Unpaid | Read |
|---|---|---|---|
| Collectible — payment method on file, or a small org billed on platform | 79 | $896,551 | Open invoices the platform should have collected. This is the actionable figure. |
| Billed out of band — no payment method and 3+ merchants | 2 | $459,094 | Excluded from the unpaid total. Enterprise contracts settled off platform; the invoice record simply is not visible here. |
| No open invoices | 308 | — | Current |
The collectible cohort sent 26,805,228 messages in 90 days. Largest exposures: Story Cannabis $180,460 (7.7M messages), Beyond Hello $135,047 (11.8M messages), Spiritleaf $133,509, Lume Co. $96,621, FIKA $75,010 (2.4M messages), Sessions Cannabis $51,454 (1.2M messages).
The backlog is persistent rather than recent — unpaid invoices appear in every one of the last twelve months, between 33 and 114 per month.
Two audiences are failing right now, both with the defect closed on 8 August, because the configuration shape that causes it is still writable.
| State | Count |
|---|---|
| completed | 3,356 |
| disabled | 1,684 |
| in progress | 9 |
| queued | 7 |
| failed | 2 |
Both carry failure_message = NULL, which is itself diagnostic: the Node Lambda writes state directly to Postgres and never runs the Rails setter that populates the message. A null message with state 3 means the query died in Redshift.
variant: greater_than value: 1 secondary_variant: month # datepart in the WRONG key secondary_value: '1'
variant: greater_than value: 1 secondary_value: '1' # datepart MISSING entirely
Working audiences on the same event type put the datepart in secondary_value. Because ParseQueryService#render substitutes an empty string for any variable it cannot resolve, the misconfiguration does not raise — it renders structurally broken SQL that travels through SQS and dies far from its cause. Captured from /aws/lambda/audience_build_members:
date_trunc('1', transaction_details.transaction_date AT TIME ZONE 'UTC' AT TIME ZONE 'America/Denver')
ERROR: Invalid datetime part for DATE_TRUNC()
Both audiences fail every day: 71378 on eight of the last seven days' runs (10, 11, 12, 13, 14, 15 and twice on 10 August), 71620 on four. Twelve failedBuildsForDlq records over seven days, every one the same signature.
Two hypotheses were tested — that webhooks fire too often, and that they carry incomplete data. The second is confirmed. The first is largely refuted, and the reason it looked true is itself the more useful finding.
The emission path is guarded at three independent levels, and the measured rate is close to the floor. webhook_member_concern.rb:32-34 checks an attribute allowlist, so a bare touch or an updated_at-only save emits nothing:
changed_values = previous_changes.keys.map(&:to_sym) deliver_webhook(:updated) if saved_changes? && (WEBHOOK_PERMITTED_ATTRIBUTES & changed_values).any?
A second gate at enqueue time returns early unless a subscribed endpoint exists, and EventWorker carries unique_for: 60.seconds. Measured over two hours against real business activity:
| Driver (2h, measured) | Count |
|---|---|
| Member rows updated at webhook merchants | 22,031 |
| Members created | 1,235 |
| Visits | 26,049 |
| Outbound webhooks emitted | 39,527 |
| Ratio per member-change | 1.70 |
| Deliveries per distinct member | 1.30 avg (35 max) |
Payloads are large: 29 to 32 fields, including points balance (ghetto_balance), consent state (allowed_email, allowed_sms, allowed_loyalty) and tier (current_milestone_tiers). What is missing is not bulk. It is meaning.
| What a receiver does not get | Consequence |
|---|---|
| No change-set — no saved_changes, no previous values | Cannot tell what changed; must diff or blind-write all ~30 fields |
| No visit reference, though a visit is the trigger | Cannot tell a purchase from a profile edit |
| No timestamp of the change | Cannot order events or detect staleness |
| No event_name at all on the pos PUT path | 42.5% of outbound volume arrives unlabelled |
This is the documented mechanism behind the known Cova full-object-PUT clobber: a receiver that cannot tell what changed must write everything back, and a partial write erases what it omits.
| POS type | event_name | Fields | Null |
|---|---|---|---|
| treez | member_updated | 29 | 14% |
| leaflogix | member_updated | 30 | 20% |
| greenline | member_updated | 30 | 30% |
| lightspeed | member_updated | 30 | 33% |
| cova | member_updated | 32 | 38% |
| klaviyo | member_created | 32 | 56% |
| pos (PUT) | absent | 8 | 25% |
Every sampled payload carried current_milestone_tiers: {spend: null, point: null, visit: null} — tier data is structurally present and empty in practice. None contained any change indicator, confirming the code reading empirically.
Of 13 declared event types, only three have any subscriber: member_updated (816 endpoints), member_created (811) and member_deleted (1). The remaining ten — campaign, message template, template image and reward grant events — still execute their callbacks and a database existence check on every relevant save, for zero recipients. visit_created and mailchimp_created are declared and never emitted at all.
| Merchant status | Endpoints | Merchants |
|---|---|---|
| lost | 395 | 364 |
| active | 389 | 361 |
| suspended | 25 | 24 |
| pending | 7 | 7 |
Four endpoints point at production.api.sringbig.technology — a misspelled domain that cannot resolve.
| # | Change | Why |
|---|---|---|
| 1 | Pass previous_changes into the payload as a changes key | Addresses the confirmed half at its root; requires serializing at event time rather than delivery time |
| 2 | Include the triggering visit id on visit-driven member_updated | Lets a receiver distinguish a purchase from a profile edit |
| 3 | Add event_name to the pos PUT path | 42.5% of volume currently arrives unlabelled |
| 4 | Filter delivery on merchant status | Half of all endpoints belong to departed accounts |
| 5 | Stop logging POS credentials to CloudWatch | Live tokens in plaintext |
| 6 | Add an active flag and a delivery-audit table | Endpoint health is currently unobservable |
| 7 | Prune the ten zero-subscriber event types | Callbacks and DB checks executing for no recipient |
Every figure in this document is a measurement, and measurements have error bars. This section records where the numbers are firm, where they are estimates, and the specific ways this investigation got things wrong before catching them — because a reader deciding what to act on needs to know which is which.
Seven working conclusions were wrong and were caught by a second check. They are listed because each one nearly reached this document as fact, and because they share a pattern worth naming.
Counts drawn directly from a full population, with no sampling or extrapolation: the endpoint and route inventories; the code census (an AST walk over 2,350 files, zero parse failures); merchant, audience, webhook-endpoint and billing counts from Postgres; the Cloudflare zone sweep across all 1,799 zones; Rollbar active-item counts, fully paginated.
| Figure | Caveat |
|---|---|
| Gateway rejections, ~918,000/day | Six-fold extrapolation from one four-hour afternoon window. Order-of-magnitude only; the four-hour counts themselves are solid. |
| Webhook volume, ~474,000/day | Twelve-fold extrapolation from a two-hour window. |
| Lambda error rates | ERROR lines per invocation, not a failure rate — batch handlers emit many lines per call, so ratios exceed 100%. |
| Rollbar resolved and unique counts | The API caps total_count at 1,000; these are floors. Rate-limit warnings mean the frontend occurrence total is also a floor. |
| Reward-grant and messaging volumes | Denormalised counters recorded at send time, not delivery-confirmed. |
| Cloudflare traffic totals | The account rollup undercounts the per-zone sweep by 40% for reasons not established. The per-zone sweep is used throughout. |
| Surface | Source | Window |
|---|---|---|
| Web tier, APM, 5xx attribution | New Relic NRQL via GraphQL | 7 days (retention limit: 8) |
| Background jobs, transaction-date, gateway rejections | CloudWatch Logs Insights | full days and 2–4h windows, stated per figure |
| Error reporting, symbolication | Rollbar API, 3 projects, fully paginated | all active items |
| POS health, merchants, audiences, billing | Production Postgres replica, read-only | 30–90 days |
| Mobile telemetry | Redshift native_app_events | all time (410.6M rows) |
| Edge | Cloudflare GraphQL, all 1,799 zones | 2026-08-09 → 08-15 |
| API contract | rails routes + apigateway get-resources | repo at 3e50175, 2026-08-13 |
| Code census | Ruby Ripper AST over 2,350 files | same commit |