Skip to content

fix(web): bound and scrub resource error telemetry - #7501

Open
Siri-Ray wants to merge 1 commit into
nexu-io:mainfrom
Siri-Ray:fix/client-resource-error-privacy
Open

fix(web): bound and scrub resource error telemetry#7501
Siri-Ray wants to merge 1 commit into
nexu-io:mainfrom
Siri-Ray:fix/client-resource-error-privacy

Conversation

@Siri-Ray

Copy link
Copy Markdown
Contributor

Why

Production stability reporting on 2026-08-25 UTC showed one device emitting 16,839 client_resource_error events from a project PDF path. The existing observer sent each failed DOM resource's full URL through the direct safety transport, so project IDs, user filenames, query strings, and external media locations could reach PostHog. The same window still contained real _next/static/chunks failures across roughly 60–70 devices, so dropping or globally sampling the event would erase an important cross-device reliability signal.

This PR makes resource-error telemetry privacy-safe and volume-bounded while preserving an immediate per-device signal for real Next.js chunk failures. It also makes repeated counts explicit so stability analysis can distinguish affected devices/resources from one resource retrying thousands of times.

What users will see

There is no visual UI change. Resource failures continue to produce safety diagnostics, but user and third-party resource URLs are reduced to low-sensitivity classifications and route templates. A repeatedly failing image, PDF, iframe, or media element no longer floods reliability telemetry.

Surface area

  • UI — new page / dialog / panel / menu item / setting / empty state in apps/web or apps/desktop (including Electron menu bar)
  • Keyboard shortcut — new or changed
  • CLI / env var — new od subcommand or flag, new tools-dev / tools-pack flag, or new OD_* env var
  • API / contract — new /api/* endpoint, new SSE event, or changed shape in packages/contracts
  • Extension point — new entry under skills/, design-systems/, design-templates/, or craft/, or change to the skills protocol
  • i18n keys — added new translation keys (see TRANSLATIONS.md for the locale workflow)
  • New top-level dependency — adding any new entry to the root package.json (dependencies or devDependencies); workspace-package package.json files are out of scope. Include a paragraph on what we get vs. what bytes we ship (see CONTRIBUTING.md → Code style)
  • Default behavior change — safety telemetry now classifies, scrubs, and window-aggregates resource errors by default
  • None — internal refactor, docs, tests, or translation update only

Screenshots

Not applicable: this changes the browser safety-telemetry pipeline and has no UI entry point.

Bug fix verification

  • Regression test: apps/web/tests/observability/resource-error.test.ts
  • Red on main:
    • privacy/classification assertions received the raw project, conversation, and file route in $current_url, with no safe resource classification;
    • five errors for the same project resource produced five ingest requests instead of one immediate event;
    • pagehide did not flush the pending repeat count.
  • Green on this branch:
    • next_chunk, product_asset, user_artifact, and external_media payloads contain only their allowed fields;
    • project IDs, filenames, query/fragment data, credentials, and external hosts/paths are absent from serialized payloads;
    • the first event is immediate and later occurrences become an exact repeat_summary after 60 seconds, successful load, pagehide, teardown, or bounded-map eviction;
    • tracking state is capped at 128 resource keys, while each device retains its own first-event signal.

Validation

  • pnpm exec vitest run -c vitest.config.ts --maxWorkers=2 tests/observability/resource-error.test.ts tests/analytics/error-tracking.test.ts tests/analytics-scrub.test.ts tests/observability/iframe-error.test.ts — 50 passed
  • pnpm --filter @open-design/web typecheck — passed
  • pnpm guard — passed
  • git diff --check — passed
  • Full pnpm --filter @open-design/web test was started but not counted as validation: it was manually interrupted after about nine minutes without a terminal summary while emitting existing jsdom canvas/navigation warnings. The focused web/analytics suites above completed cleanly.
  • Local runtime was Node 24.16.0; the workspace warned that its exact target is Node 24.18.0.

@lefarcen
lefarcen requested a review from mrcfps August 27, 2026 09:56
@lefarcen lefarcen added size/L PR changes 300-700 lines risk/medium Medium risk: regular code changes type/bugfix Bug fix skip-validation Maintainer override: bot will not auto-add needs-validation on this PR. labels Aug 27, 2026

@mrcfps mrcfps 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.

@Siri-Ray Thanks for tackling both the privacy exposure and the telemetry flood in one focused change. The overall classification/windowing direction is sound, but this pass found two cases that still violate the PR's core privacy and per-resource counting guarantees. I've left concrete fixes inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

});
crossorigin_mode: normalizeCrossorigin(target.getAttribute('crossorigin')),
};
const key = `${tag}\0${resource.normalized_resource}`;

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.

Blocking — keep the private resource identity separate from the scrubbed telemetry value. This key uses normalized_resource, but normalization intentionally collapses every same-tag project PDF on the same route to one value such as /api/projects/:project_id/raw/:file.pdf. Consequently, errors for two distinct PDFs share one window: the second PDF loses its immediate first event, and a successful load of either PDF closes the other's repeat window. Alternating a failing resource with loads of another colliding resource can also create fresh immediate events repeatedly, undermining the volume bound. This conflicts with the PR's stated goal of distinguishing affected resources from retries of one resource. Please derive a local-only identity from the actual resource (for example, a canonical origin/path value that is never copied into event properties), use it consistently in both the error and load lookups, and add a regression test with two private PDF URLs that normalize identically to prove they get independent first/count/load behavior.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment on lines +221 to +254
const type = resourceType(tag);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return privateResource(parsed.protocol.replace(':', '') || 'opaque', tag);
}
const extension = resourceExtension(parsed.pathname);

if (parsed.origin !== window.location.origin) {
return {
category: 'external_media',
normalized_resource: `external:${type}${extension ? `.${extension}` : ''}`,
origin_relation: 'cross_origin',
resource_extension: extension,
resource_route: 'external',
resource_type: type,
};
}

if (parsed.pathname.startsWith('/_next/static/chunks/')) {
const chunkId = safeNextChunkId(parsed.pathname);
return {
category: 'next_chunk',
normalized_resource: `next:${chunkId}`,
origin_relation: 'same_origin',
resource_extension: extension,
resource_route: '/_next/static/chunks/:chunk',
resource_type: type,
};
}

const productRoute = productAssetRoute(parsed.pathname);
if (productRoute) {
return {
category: 'product_asset',
normalized_resource: parsed.pathname,

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.

Blocking — these branches still serialize user-controlled URL text. The non-HTTP branch copies an arbitrary custom scheme into resource_route/normalized_resource, and the product-asset branch copies the full parsed.pathname. Prefix classification does not prove the remainder is a shipped asset: a failed same-origin URL such as /fonts/Customer%20Contract.pdf is classified as product_asset and sends that filename verbatim; similarly, customer-project-123:payload sends customer-project-123. That directly breaks this PR's guarantee that filenames and project identifiers are absent from serialized telemetry. Please make opaque schemes fixed labels and make product assets emit only the route template/type/allowlisted extension (or validate against an exact owned manifest); retain a chunk identifier only under a genuinely restrictive bundler-generated format. Add adversarial fixtures for a private filename beneath a product-looking prefix and a custom scheme, asserting the serialized payload contains neither input.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@github-actions

Copy link
Copy Markdown
Contributor

Visual regression review

Head: 7f19b5a · Base: eef3f10

0 changed · 53 unchanged · 0 new without baseline · 0 failed

Unchanged cases
Case Main PR Diff
visual-avatar-local-agent-list
0 px (0.00%)
main pr diff
visual-avatar-local-agent-list-panel
0 px (0.00%)
main pr diff
visual-avatar-menu
0 px (0.00%)
main pr diff
visual-avatar-menu-panel
0 px (0.00%)
main pr diff
visual-avatar-open-design-model-picker
0 px (0.00%)
main pr diff
visual-critical-settings
0 px (0.00%)
main pr diff
visual-critical-workspace
0 px (0.00%)
main pr diff
visual-critical-workspace-preview
0 px (0.00%)
main pr diff
visual-deepseek-unpaid-campaign-600
0 px (0.00%)
main pr diff
visual-deepseek-unpaid-campaign-short-height
0 px (0.00%)
main pr diff
visual-design-system-detail
1 px (0.00%)
main pr diff
visual-design-systems
1 px (0.00%)
main pr diff
visual-home
0 px (0.00%)
main pr diff
visual-home-catalog
0 px (0.00%)
main pr diff
visual-home-context-picker
0 px (0.00%)
main pr diff
visual-home-context-picker-popover
0 px (0.00%)
main pr diff
visual-home-plugin-filter
0 px (0.00%)
main pr diff
visual-home-plugin-use-staged
0 px (0.00%)
main pr diff
visual-home-plugin-use-with-query
0 px (0.00%)
main pr diff
visual-home-staged-attachment
0 px (0.00%)
main pr diff

Visual diff is advisory only and does not block merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/medium Medium risk: regular code changes size/L PR changes 300-700 lines skip-validation Maintainer override: bot will not auto-add needs-validation on this PR. type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants