Skip to content

Conversation

@rodionchernyshov
Copy link
Collaborator

@rodionchernyshov rodionchernyshov commented Nov 24, 2025

Summary by CodeRabbit

  • New Features

    • Frontend and backend observability: global error handling, error capture, page/performance metrics, websocket and interaction timing, route/component lifecycle and user/context tagging.
    • UI metrics and safe reporting: automatic routing and interaction metrics, error/replay sampling, and optional Sentry integration with graceful fallbacks.
  • Chores

    • Added observability configuration, build-time flags, environment settings, and Sentry dependencies for client and server.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Adds end-to-end observability: new frontend/backend registries and providers, Sentry adapters, metric primitives and safe wrappers, global UI error handling, build/runtime Sentry config, instrumentation points (metrics, traces, error captures) and runtime wiring for UI and ASGI server.

Changes

Cohort / File(s) Summary
Project deps & build config
pyproject.toml, src/ui/package.json, src/ui/.env, src/ui/vite.config.ts
Add backend dep sentry-sdk[fastapi]; add frontend dep @sentry/vue; introduce frontend .env Sentry keys; expose VITE_SENTRY_* defines in Vite config.
Frontend observability core
src/ui/src/observability/base.ts, src/ui/src/observability/index.ts
Add ObservabilityProvider interface and ObservabilityRegistry; create default observabilityRegistry and attempt conditional registration of a SentryAdapter.
Frontend metrics & adapter
src/ui/src/observability/frontendMetrics.ts, src/ui/src/observability/sentryAdapter.ts
New metric enums/types, safe wrappers incrementMetricSafely / recordDistributionSafely, metric tracking functions, and a SentryAdapter implementing provider behavior with dynamic import and init.
Frontend integration & startup
src/ui/src/main.ts, src/ui/src/core/index.ts, src/ui/src/core/navigation.ts
Initialize observability at app startup, setup global error handling and page-load tracking; add WebSocket timing/metrics, route-change and component add/delete metrics.
Frontend error handling & logging
src/ui/src/composables/useGlobalErrorHandling.ts, src/ui/src/composables/useLogger.ts
New global error handling setup/teardown; useLogger forwards warnings/errors to provider captures while preserving console output.
Frontend tracking hook
src/ui/src/composables/useWriterTracking.ts
After tracking events, increment namespaced metrics (user_action.{eventName}) via safe metric helper.
Frontend misc
src/ui/src/composables/useBlueprintNodeTools.ts
Minor formatting (trailing newline).
Frontend build & env
src/ui/.env, src/ui/vite.config.ts
Add VITE_SENTRY_* env keys and surface them to the build define block.
Backend observability core
src/writer/observability/base.py, src/writer/observability/__init__.py
Add ObservabilityProvider ABC, ObservabilityRegistry, global registry and get_registry() entry that can register SentryAdapter dynamically.
Backend Sentry adapter & wiring
src/writer/observability/sentry_adapter.py, src/writer/serve.py, src/writer/app_runner.py, src/writer/blueprints.py, src/writer/core.py
New SentryAdapter initializing sentry_sdk, clamping trace/sample rates, redacting sensitive headers, FastAPI middleware instrumentation; runtime wiring to initialize/instrument app and capture errors/scopes in key exception handlers.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Frontend App
    participant ObsRegFE as ObservabilityRegistry (FE)
    participant SentryFE as SentryAdapter (FE)
    participant SentrySDK as "@sentry/vue"

    App->>ObsRegFE: initializeProvider(app, router)
    ObsRegFE->>SentryFE: resolve & initialize()
    alt DSN present & enabled
        SentryFE->>SentrySDK: dynamic import & init(config)
        SentrySDK-->>SentryFE: ready
        SentryFE-->>ObsRegFE: initialized
    else missing/disabled
        SentryFE-->>ObsRegFE: no-op / logged
    end
    Note right of App: later calls use ObsRegFE.getInitializedProvider()
Loading
sequenceDiagram
    autonumber
    participant User as Client Action
    participant Core as Core (WebSocket)
    participant Metrics as frontendMetrics
    participant ObsRegFE as ObservabilityRegistry
    participant SentryFE as SentryAdapter

    User->>Core: sendFrontendMessage(type, payload, cb)
    Core->>Core: record start timestamps
    Core->>Metrics: record/send WebSocketMessageDuration
    Metrics->>ObsRegFE: getInitializedProvider()
    ObsRegFE->>SentryFE: recordDistribution / incrementMetric
    alt callback/error
        Core->>Metrics: increment WebSocketMessageError
        Metrics->>ObsRegFE: incrementMetric(websocket.message_error)
    end
Loading
sequenceDiagram
    autonumber
    participant Server as ASGI startup
    participant ObsRegBE as ObservabilityRegistry (BE)
    participant SentryBE as SentryAdapter (BE)
    participant FastAPI as FastAPI App

    Server->>ObsRegBE: get_registry(app_path)
    ObsRegBE->>SentryBE: register & initialize()
    alt initialized & enabled
        SentryBE->>FastAPI: instrument_fastapi_app(app)
        Note right of FastAPI: per-request Sentry scope, tags, capture exceptions
    else not initialized
        Note right of Server: continue without instrumentation
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Pay extra attention to:
    • src/ui/src/observability/sentryAdapter.ts (dynamic imports, tracing integration, metric emission).
    • src/writer/observability/sentry_adapter.py (sentry_sdk init, sampling/clamping, before_send redaction, FastAPI middleware).
    • Initialization ordering and error handling in src/ui/src/main.ts, src/ui/src/core/index.ts, and src/writer/serve.py.
    • Metric helper contracts and safe wrappers (src/ui/src/observability/frontendMetrics.ts) for consistent tags and resilient failure handling.

Poem

🐰 I hopped in to catch each silent squeak,

Traces twinkle where the errors speak;
Metrics tally every tiny hop,
From client burrow to server top;
Now every bug gets found—quick peek!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.19% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'AB-738 Implement FE/BE crash reports & metrix' clearly summarizes the main changes: adding frontend/backend crash reporting and metrics collection via Sentry integration.
Linked Issues check ✅ Passed The PR implements comprehensive observability via Sentry for both frontend and backend, including crash reporting (captureException), error tracking (setupGlobalErrorHandling), and metrics collection (increment/distribution metrics), addressing the AB-738 objective.
Out of Scope Changes check ✅ Passed All changes directly support the crash reports and metrics objectives: observability adapters, Sentry integration, metric tracking utilities, error handlers, and instrumentation across UI/backend components are all on-scope.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AB-738

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (17)
src/ui/vite.config.ts (1)

24-26: Consider a lower default for traces sample rate in production.

The default VITE_SENTRY_TRACES_SAMPLE_RATE of 1.0 (100%) could generate excessive trace data and impact performance in high-traffic scenarios. Consider defaulting to a lower value like 0.1 (10%).

Apply this diff:

-		VITE_SENTRY_TRACES_SAMPLE_RATE: JSON.stringify(
-			process.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "1.0",
-		),
+		VITE_SENTRY_TRACES_SAMPLE_RATE: JSON.stringify(
+			process.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
+		),
src/ui/.env (1)

4-4: Consider reducing trace sample rate for production.

A VITE_SENTRY_TRACES_SAMPLE_RATE of 1.0 (100%) in production can generate significant overhead and costs. Consider using a lower value such as 0.1 (10%) for production environments.

Apply this diff:

-VITE_SENTRY_TRACES_SAMPLE_RATE=1.0
+VITE_SENTRY_TRACES_SAMPLE_RATE=0.1
src/ui/src/main.ts (1)

15-16: Frontend observability wiring looks solid; consider guarding against duplicate global handlers and normalizing error types

The observability provider init, page-load metric, and global error handlers are all wired in a safe, non-fatal way, which is good. Two small follow-ups you might consider:

  • Because load() can be retried from initialise(), if it ever throws after the listeners are attached, you'll add multiple window listeners. A simple one-time guard (e.g., a module-level flag) around the addEventListener calls would prevent duplicates.
  • For the "error" handler you almost always end up with errorType === error.name (e.g., "Error", "TypeError"), so the "window_error" fallback is rarely used. If you care about distinguishing sources, you might want to pass an explicit error type (e.g., "window_error" / "unhandled_promise_rejection") in both handlers and rely less on error.name.

Also applies to: 64-92

src/ui/src/observability/index.ts (1)

1-15: Static import means missing Sentry deps will fail before the try block

The registry setup and guarded register("sentry", new SentryAdapter()) look fine. Note, though, that because SentryAdapter is imported statically at the top of the module, missing adapter dependencies (e.g., @sentry/vue) will cause a module‑load failure before the try/catch runs. If you truly want Sentry to be optional at runtime, you may want to move the adapter import behind a dynamic import or make SentryAdapter itself handle missing external deps internally.

src/ui/src/core/index.ts (2)

191-201: WebSocket keepalive latency metric is correct; consider guarding performance for non-browser contexts

Measuring latency around "keepAlive" via performance.now() and trackWebSocketLatency is a good fit here. If generateCore() is ever used in non‑browser or headless test environments, you might want to guard against a missing performance global (e.g., if (typeof performance === "undefined")), but in standard browser-only usage this is fine.


734-846: Message duration and interaction metrics are well placed; minor simplifications possible

The timing and metric recording in sendFrontendMessage are wired correctly:

  • wrappedCallback tracks interaction duration for non‑keepAlive messages.
  • websocket.message_duration and websocket.message_error are sent only when a provider with the right capability is initialized, and metric failures are swallowed.

Two small cleanups you could consider:

  • messageStartTime and interactionStartTime are captured back‑to‑back; unless you intentionally want them to differ, a single timestamp reused for both would simplify things.
  • The repeated getInitializedProvider() + structural cast pattern is cropping up in several places; long‑term, a small helper around recordDistribution/incrementMetric on top of the registry could reduce boilerplate.
src/writer/observability/__init__.py (1)

16-32: Narrow the broad Exception catch and avoid repeated __all__ mutations

Catching Exception in _register_sentry_adapter avoids startup failures, but it also masks real programming errors inside SentryAdapter (not just missing imports). Similarly, calling get_registry() repeatedly can append "SentryAdapter" to __all__ multiple times.

You might tighten this up by:

  • Catching ImportError separately for the optional dependency case, and logging other exceptions with exc_info=True so they’re visible.
  • Guarding __all__ updates and registration so they only happen once (e.g., by checking whether "sentry" is already registered or "SentryAdapter" is already in __all__).
src/ui/src/observability/frontendMetrics.ts (3)

3-12: Consider modern Navigation Timing API for page load metrics

window.performance.timing is legacy and may be phased out in some environments. Consider preferring performance.getEntriesByType("navigation")[0] when available and falling back to performance.timing to future‑proof this helper.


13-31: Deduplicate metrics method typing via a shared interface

Each helper redefines a local type for recordDistribution / incrementMetric. This is correct but brittle if signatures change. Consider defining a shared MetricsObservabilityProvider (e.g., extending ObservabilityProvider with optional incrementMetric?, recordDistribution?, setGauge?) and reusing it here instead of ad‑hoc casts.

Also applies to: 45-54, 75-84, 103-112, 134-143


55-62: Revisit error_message as a metric tag to avoid high cardinality/PII

Including (even truncated) error_message as a tag can create very high cardinality series and may duplicate potentially sensitive content already sent in error events themselves. Consider limiting the metric to error_type (plus maybe a coarse error code) and moving full messages into Sentry events only.

src/ui/src/observability/sentryAdapter.ts (5)

114-116: Align isEnabled with DSN presence to avoid noisy failed inits

Currently isEnabled returns true unless VITE_SENTRY_ENABLED === "false", so the registry will pick Sentry even when no DSN is configured, only to have initialize log and return false. Consider treating “missing DSN” as effectively disabled so the registry can fall back cleanly:

-	isEnabled(): boolean {
-		return import.meta.env[SENTRY_ENABLED_ENV] !== "false";
-	}
+	isEnabled(): boolean {
+		const enabledFlag = import.meta.env[SENTRY_ENABLED_ENV];
+		const dsn = import.meta.env[SENTRY_DSN_ENV];
+		if (enabledFlag === "false") return false;
+		// Only consider Sentry enabled when a DSN is configured
+		return !!dsn;
+	}

This keeps behavior explicit while avoiding repeated failed initialization attempts.

Also applies to: 131-139


200-209: Harden sample‑rate parsing and clamp to a safe range

parseFloat on env values can yield NaN or out‑of‑range numbers, which then get passed directly to Sentry. Consider normalizing:

-			const tracesSampleRate = parseFloat(
-				import.meta.env[SENTRY_TRACES_SAMPLE_RATE_ENV] || "1.0",
-			);
-			const replaySampleRate = parseFloat(
-				import.meta.env[SENTRY_REPLAY_SAMPLE_RATE_ENV] || "0.1",
-			);
+			const normalizeRate = (value: string, fallback: number): number => {
+				const parsed = parseFloat(value);
+				if (Number.isNaN(parsed)) return fallback;
+				return Math.min(Math.max(parsed, 0), 1);
+			};
+
+			const tracesSampleRate = normalizeRate(
+				import.meta.env[SENTRY_TRACES_SAMPLE_RATE_ENV] || "1.0",
+				1.0,
+			);
+			const replaySampleRate = normalizeRate(
+				import.meta.env[SENTRY_REPLAY_SAMPLE_RATE_ENV] || "0.1",
+				0.1,
+			);

This guards against misconfigured env values.


306-312: Guard access to window/navigator for SSR or non‑browser environments

captureException, captureMessage, and _setInitialMetadata assume window and navigator exist. If this adapter is ever imported or exercised in SSR/tests without a DOM, these calls will throw. A small guard makes it safer:

-			const runtimeContext = {
-				url: window.location.href,
-				userAgent: navigator.userAgent,
-			};
+			if (typeof window === "undefined" || typeof navigator === "undefined") {
+				// Non‑browser environment; skip browser‑specific context.
+				this.sentry.captureException(error instanceof Error ? error : new Error(String(error)));
+				return;
+			}
+			const runtimeContext = {
+				url: window.location.href,
+				userAgent: navigator.userAgent,
+			};

and similarly gate window.location, navigator.userAgent, and navigator.language in other methods.

Also applies to: 355-367, 438-442


469-488: Clarify route metric ownership vs. external callers

_setupRouteTracking already increments frontend_route_changes_total on router afterEach. Combined with trackRouteChange in frontendMetrics.ts, there’s potential for duplicated metrics if both are wired. It would help to:

  • Decide whether router‑level tracking is the sole producer of this metric, and
  • Document (or enforce) that external callers should not emit the same metric, or use a distinct name.

This keeps route metrics consistent and avoids double counting.


559-567: Cache metrics instance after lazy lookup to avoid repeated discovery

_getMetricsInstance falls back to _getMetrics(this.sentryModule) on every call but doesn’t cache the result. Minor, but you can avoid redundant lookups:

-		if (this.sentryModule) {
-			return this._getMetrics(this.sentryModule);
-		}
-		return null;
+		if (this.sentryModule) {
+			this.metrics = this.metrics ?? this._getMetrics(this.sentryModule);
+			return this.metrics;
+		}
+		return null;

This keeps the lazy semantics while reusing the discovered metrics handle.

src/ui/src/observability/base.ts (2)

1-21: Consider adding optional metric APIs to ObservabilityProvider

Callers like frontendMetrics and SentryAdapter rely on incrementMetric / recordDistribution / setGauge via ad‑hoc casts and "in" checks. To make this contract explicit and reduce duplication, consider extending the interface (or adding a sub‑interface) with optional metric methods:

export interface ObservabilityProvider {
  // existing methods...
  incrementMetric?(
    name: string,
    options?: { tags?: Record<string, string>; unit?: string; value?: number },
  ): void;
  recordDistribution?(
    name: string,
    value: number,
    options?: { tags?: Record<string, string>; unit?: string },
  ): void;
  setGauge?(
    name: string,
    value: number,
    options?: { tags?: Record<string, string>; unit?: string },
  ): void;
}

This keeps the base type aligned with how providers are actually used.


87-129: Review comment is valid and recommended for implementation

The suggestion to normalize initializeProvider to an always-async API is sound. Verification shows:

  • Single call site at src/ui/src/main.ts:65 already uses await, making it future-proof
  • Provider interface at src/ui/src/observability/base.ts:2 currently allows boolean | Promise<boolean>, but the only implementation (SentryAdapter) returns Promise<boolean>
  • Minimal impact: No test files affected, only one real usage
  • Code simplification: Eliminates the result instanceof Promise branching logic and reduces complexity

To implement this refactoring, update:

  1. ObservabilityRegistry.initializeProviderasync initializeProvider(...): Promise<boolean>
  2. ObservabilityProvider interface → initialize(...): Promise<boolean> or mark as async
  3. Replace the if/else branching with const result = await provider.initialize(app, router);
  4. The call site (main.ts) is already compatible
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c3982b3 and 0a56d36.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • pyproject.toml (1 hunks)
  • src/ui/.env (1 hunks)
  • src/ui/package.json (1 hunks)
  • src/ui/src/composables/useLogger.ts (1 hunks)
  • src/ui/src/composables/useWriterTracking.ts (2 hunks)
  • src/ui/src/core/index.ts (4 hunks)
  • src/ui/src/core/navigation.ts (3 hunks)
  • src/ui/src/main.ts (4 hunks)
  • src/ui/src/observability/base.ts (1 hunks)
  • src/ui/src/observability/frontendMetrics.ts (1 hunks)
  • src/ui/src/observability/index.ts (1 hunks)
  • src/ui/src/observability/sentryAdapter.ts (1 hunks)
  • src/ui/vite.config.ts (1 hunks)
  • src/writer/observability/__init__.py (1 hunks)
  • src/writer/observability/base.py (1 hunks)
  • src/writer/observability/sentry_adapter.py (1 hunks)
  • src/writer/serve.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-22T11:08:20.053Z
Learnt from: VladKrasny
Repo: writer/writer-framework PR: 1100
File: src/ui/src/composables/useFilesEncoder/useFilesEncoder.ts:14-22
Timestamp: 2025-08-22T11:08:20.053Z
Learning: The writer-framework project does not require SSR (Server-Side Rendering) support, so using `window` object is acceptable in the codebase.

Applied to files:

  • src/ui/vite.config.ts
🧬 Code graph analysis (10)
src/ui/src/observability/index.ts (1)
src/ui/src/observability/base.ts (1)
  • ObservabilityRegistry (23-161)
src/ui/src/main.ts (2)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/ui/src/observability/frontendMetrics.ts (2)
  • trackPageLoadTime (3-38)
  • trackError (40-68)
src/ui/src/composables/useLogger.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/writer/observability/base.py (1)
src/ui/src/observability/base.ts (3)
  • ObservabilityProvider (1-21)
  • ObservabilityRegistry (23-161)
  • register (27-34)
src/writer/serve.py (2)
src/writer/observability/__init__.py (1)
  • get_registry (30-32)
src/writer/observability/base.py (2)
  • initialize_provider (51-84)
  • instrument_app (86-94)
src/ui/src/core/navigation.ts (1)
src/ui/src/observability/frontendMetrics.ts (1)
  • trackRouteChange (98-124)
src/ui/src/observability/frontendMetrics.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/writer/observability/sentry_adapter.py (1)
src/writer/observability/base.py (4)
  • ObservabilityProvider (8-27)
  • is_enabled (25-27)
  • initialize (12-14)
  • instrument_fastapi_app (17-19)
src/ui/src/composables/useWriterTracking.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/ui/src/core/index.ts (3)
src/ui/src/observability/frontendMetrics.ts (2)
  • trackWebSocketLatency (70-96)
  • trackInteractionDuration (126-159)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/ui/src/writerTypes.ts (1)
  • Component (25-44)
🪛 dotenv-linter (4.0.0)
src/ui/.env

[warning] 5-5: [UnorderedKey] The VITE_SENTRY_REPLAY_SAMPLE_RATE key should go before the VITE_SENTRY_TRACES_SAMPLE_RATE key

(UnorderedKey)

🪛 Ruff (0.14.5)
src/writer/serve.py

172-172: Do not catch blind exception: Exception

(BLE001)

src/writer/observability/sentry_adapter.py

69-69: Unused function argument: hint

(ARG001)


110-110: Consider moving this statement to an else block

(TRY300)


161-161: Consider moving this statement to an else block

(TRY300)

src/writer/observability/__init__.py

26-26: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.9)
  • GitHub Check: build (3.10)
  • GitHub Check: tests (chromium)
  • GitHub Check: tests (firefox)
  • GitHub Check: tests (webkit)
🔇 Additional comments (11)
src/ui/src/core/navigation.ts (1)

1-1: LGTM! Route change tracking is well integrated.

The implementation correctly tracks route changes at all appropriate points (explicit hash changes and browser hashchange events), with proper error handling and sensible defaults.

Also applies to: 92-110

src/ui/src/composables/useWriterTracking.ts (1)

189-212: LGTM! Observability metrics integration is well implemented.

The metric tracking is appropriately added alongside existing event tracking, with proper error handling and structured metric naming (user_action.{eventName}).

src/writer/serve.py (1)

167-173: LGTM! Observability initialization is appropriately defensive.

The optional observability setup correctly ensures that failures don't break the application startup. The broad exception handling on line 172 is intentional and appropriate here, as observability is a non-critical feature that should never prevent the application from starting.

src/ui/src/composables/useLogger.ts (1)

3-3: LGTM! Logger observability integration is clean and non-breaking.

The integration maintains full backward compatibility while adding observability capture for warnings and errors. The implementation correctly handles different argument types and includes useful metadata.

Also applies to: 8-42

pyproject.toml (1)

57-57: Consider updating the Sentry SDK to a more recent version within the ^2.x range.

The current constraint ^2.15.0 is free from the known security vulnerabilities in the GitHub security advisories (those affect versions < 2.8.0 or < 1.14.0). However, version 2.15.0 is significantly outdated—the latest available is 2.46.0. Updating to allow newer patch and minor versions will ensure access to the latest bug fixes, performance improvements, and security patches. You can update the constraint to ^2.15.0 or simply allow updates to ^2.46.0 depending on your stability requirements.

src/ui/package.json (1)

51-51: No issues found—@sentry/vue@10.25.0 is compatible with Vue 3.5.0 and no security advisories detected.

The package version is compatible with Vue 3.5.0, and the latest available version is 10.26.0. The caret constraint (^10.25.0) permits automatic updates to the latest minor version, so no action is required.

src/ui/src/main.ts (2)

164-171: Good use of observability registry for initialization failures

Capturing the core initialization failure via observabilityRegistry.captureException with structured context (source, component, stage) is a clean way to surface fatal startup issues without coupling directly to Sentry.


102-108: Remove the review comment—this change is correct and fixes a type mismatch

The verification shows:

  • WriterApiUserProfile.id is type number
  • UserCollaborationPing.userId expects type number

The new code correctly passes writerProfile.id (a number) directly to updateOutgoingPing. The old code with .toString() would have sent a string, which violates the type contract. This change aligns the code with the correct types—it's a bug fix, not a breaking change.

src/ui/src/core/index.ts (1)

848-878: Component add/delete metrics look correct and safe

Emitting component.deleted and component.added metrics with a component_type tag right where components are mutated is a good hook point. The component existence check before deletion and the defensive try/catch around the metric call avoid surprises if no observability provider is initialized or if the provider throws.

Also applies to: 880-907

src/writer/observability/sentry_adapter.py (1)

119-178: FastAPI middleware integration is sound; be mindful of when middleware can be added

The custom SentryMiddleware correctly:

  • Uses push_scope() per request.
  • Tags request/response metadata and captures exceptions via sentry_sdk.capture_exception.
  • Handles missing FastAPI/Starlette/imports and already‑started apps without breaking startup.

The control flow and exception handling look good. Once you address the PII handling in the request context (comment above), this will be a solid integration.

src/writer/observability/base.py (1)

8-99: Backend observability registry implementation looks solid

The provider abstraction and registry lifecycle (env selection, first‑enabled fallback, initialization, and instrumentation) are straightforward and mirror the frontend design. Error paths are logged and safely handled; no correctness issues stand out.

@@ -0,0 +1,5 @@
VITE_SENTRY_DSN=https://6c13c3260114c5b1ceb3b6ae58d0cdb3@o1026471.ingest.us.sentry.io/4510398008393728
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify .env file is not committed to version control.

The Sentry DSN is exposed in this .env file. While the DSN is a public key, environment files should typically not be committed to version control as they can contain sensitive configuration. Ensure this file is properly listed in .gitignore, or consider using environment variables at runtime instead.


🏁 Script executed:

#!/bin/bash
# Check if .env is in .gitignore
grep -r "^\.env$" .gitignore

# Check if this .env file is tracked by git
git ls-files src/ui/.env

Length of output: 11


Let me verify the .gitignore status to ensure we have complete information:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check .gitignore content directly
cat .gitignore | grep -E "\.env"

Length of output: 42



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for .gitignore files anywhere in repo
fd -name .gitignore

# Also check if there's a root-level gitignore (alternative names)
ls -la | grep -i git

Length of output: 407



---

🏁 Script executed:

```shell
#!/bin/bash
# Check .gitignore content for .env entries
cat .gitignore

Length of output: 425


Remove src/ui/.env from version control and add it to .gitignore.

The .env file containing the Sentry DSN is currently tracked by git and is not listed in .gitignore. This violates security best practices and exposes environment configuration in version control history.

Add .env to .gitignore and remove the file from git tracking using git rm --cached src/ui/.env. Consider using a .env.example template instead for documenting required environment variables.

🤖 Prompt for AI Agents
In src/ui/.env at line 1, the committed .env contains a Sentry DSN and must be
removed from version control; add src/ui/.env (or just .env) to .gitignore, run
git rm --cached src/ui/.env to stop tracking the file while keeping it locally,
commit the .gitignore change and the removal, and optionally add a
src/ui/.env.example with placeholder variable names (e.g. VITE_SENTRY_DSN=) to
document required environment variables.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You need to use config.js. You can take a look at this PR which does that: https://github.com/WriterInternal/fs.agent-flow/pull/89 .

IMO, it needs to be a new hook like

function useWriterConfiguration(core: Core) {
  onMounted(() => {
    if (wf.isWriterCloudApp.value) fetchConfig()
  });

  return config;
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't you use Sentry SDK for Vue ?


self._agent_id = os.getenv("WRITER_APP_ID")
self._org_id = os.getenv("WRITER_ORG_ID")
environment = os.getenv(SENTRY_ENVIRONMENT_ENV, "production")
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't have this environment variable, so it'll be prodduction for Test and dev environment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can use WRITER_BASE_URL?

Copy link

@pullrequest pullrequest bot left a comment

Choose a reason for hiding this comment

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

HackerOne Code Security Review

🟢 Scan Complete: 23 Issue(s)

Here's how the code changes were interpreted and info about the tools used for scanning.

📖 Summary of Changes The changes introduce comprehensive observability and error tracking across the application using Sentry. New modules and configurations were added to both frontend (Vue/TypeScript) and backend (Python) components to enable error monitoring, performance tracking, and metrics collection. The implementation includes global error handling, metric tracking for various events, and Sentry SDK integration with detailed context and logging capabilities.
File Summary
pyproject.toml Added sentry-sdk dependency with FastAPI extras to the poetry dependencies section, which appears to be the only change in the configuration file.
src/ui/.env A new .env file was added with Sentry configuration settings, including DSN, environment, and sampling rates for error tracking and performance monitoring in a production context.
src/ui/package.json A new dependency "@sentry/vue" with version "^10.25.0" was added to the dependencies section of the package.json file.
src/ui/src/composables/useBlueprintNodeTools.ts No changes were made to this file. The old and new versions are identical, with an extra newline added at the end of the new version.
src/ui/src/composables/useGlobalErrorHandling.ts A new file is introduced that implements global error handling for web applications, with functions to set up and tear down error event listeners, extract and track errors, and handle both window errors and unhandled promise rejections.
src/ui/src/composables/useLogger.ts The updated file adds an import for observabilityRegistry and enhances warn and error methods to capture messages and exceptions using an observability provider, with additional metadata and error handling.
src/ui/src/composables/useWriterTracking.ts The file now includes an import for frontend metrics and adds a call to incrementMetricSafely within the track function, which increments a metric with the event name and includes additional metadata about the event type.
src/ui/src/core/index.ts Added observability metrics to WebSocket message handling and component management functions, including tracking WebSocket latency, message duration, interaction duration, and component addition/deletion events.
src/ui/src/core/navigation.ts Added import for route change tracking metric, and introduced a new trackRouteChange function that is called in changePageInHash and changeRouteVarsInHash methods. Also added a hashchange event listener to track route changes when window is available.
src/ui/src/main.ts Added observability-related imports and functions, including global error handling setup, observability registry initialization, page load time tracking, and exception capturing during core initialization.
src/ui/src/observability/base.ts The file introduces a new TypeScript module defining an ObservabilityProvider interface and an ObservabilityRegistry class for managing and initializing observability providers with methods for registering, initializing, and capturing exceptions and messages.
src/ui/src/observability/frontendMetrics.ts A new TypeScript file was added defining enums for metric names, types, and units, along with utility functions for tracking various frontend metrics like page load time, errors, WebSocket latency, route changes, and interaction durations.
src/ui/src/observability/index.ts A new file was created that imports and exports ObservabilityRegistry and SentryAdapter, initializes an observabilityRegistry, and attempts to register a Sentry adapter with error handling via a debug console log.
src/ui/vite.config.ts Added Sentry configuration environment variables to the define section, with default values for DSN, traces sample rate, and replay sample rate.
src/writer/app_runner.py Added Sentry initialization in the app process's _main method with error handling and logging. The new code attempts to initialize the Sentry provider and logs success or failure messages, providing observability for potential initialization issues.
src/writer/blueprints.py Added Sentry error tracking for blueprint execution errors, with detailed context and tags for better error monitoring and debugging. Introduced a logger and modified exception handling to capture and report errors more comprehensively.
src/writer/core.py The changes in the file primarily involve adding Sentry error tracking in the _handle_global_event method of the EventHandler class. This includes importing and using Sentry SDK to capture and log exceptions with additional context when an error occurs during global event handling.
src/writer/observability/init.py A new observability integration module was created, introducing an ObservabilityRegistry, a Sentry adapter registration function, and a method to retrieve the registry with optional app path configuration.
src/writer/observability/base.py The file introduces a new observability framework with an abstract base class ObservabilityProvider and a registry class ObservabilityRegistry, providing mechanisms for registering, initializing, and instrumenting observability providers for applications.
src/writer/observability/sentry_adapter.py The file is a new implementation of a Sentry observability provider adapter for a Python application. It includes methods for initializing Sentry, checking if Sentry is enabled, instrumenting FastAPI applications, and configuring Sentry SDK with metadata, logging integration, and error handling.
src/writer/serve.py Added observability initialization with Sentry error tracking in the get_asgi_app function, and modified the blueprint job creation endpoint to capture exceptions with Sentry. Also added Sentry context and tags for better error tracking and debugging.
ℹ️ Issues Detected

NOTE: These may not require action!

Below are unvalidated results from the Analysis Tools that ran during the latest scan for transparency. We investigate each of these for accuracy and relevance before surfacing them as a potential problem.

How will I know if something is a problem?
When validation completes, any concerns that warrant attention prior to merge will be posted as inline comments. These will show up in 2 ways:

  • Expert review (most cases): Issues will be posted by experts who manually reviewed and validated them. These are real HackerOne engineers (not bots) reviewing through an integrated IDE-like tool. You can communicate with them like any other reviewer. They'll stay assigned and get notified with commit & comment updates.
  • Automatically: In cases where our validation checks have highest confidence the problem is legitimate and urgent. These will include a description of contextual reasoning why & actionable next steps.
File & Line Issue
src/ui/.env Line 1 This file contains a Sentry DSN which includes a project identifier and authentication token. While this appears to be intended for client-side error reporting, exposing DSNs in version control is generally not recommended as they should be managed through environment variables or deployment configurations.
src/writer/core.py Line 1900 The code introduces a new error handling mechanism that uses Sentry SDK to capture exceptions. However, it directly uses user-controlled data from ev.type and ev.handler in the Sentry context without proper validation or sanitization. This could potentially lead to injection of malicious data into Sentry logs or dashboards if an attacker can control these values. Additionally, the code catches all exceptions when trying to use Sentry, which could hide important security-related errors.
src/writer/observability/sentry_adapter.py Line 164 The SentryMiddleware implementation at lines 143-174 includes sensitive request data in the Sentry context. While there's an attempt to filter sensitive headers in the before_send function (lines 76-84), the middleware itself adds query parameters (line 164) to the Sentry context without filtering potentially sensitive information that might be present in those parameters.
src/ui/src/observability/sentryAdapter.ts Line 195 The code hardcodes 'production' as the environment value in the Sentry initialization config (line 195), regardless of the actual environment the application is running in. This could lead to development or staging errors being reported as production issues, potentially causing confusion in error tracking and metrics.
src/writer/app_runner.py Line 504 The code initializes Sentry (an error tracking service) without proper validation of user input. The app_path parameter is passed directly to the observability registry without sanitization, which could potentially lead to path traversal if the app_path is manipulated. An attacker could potentially use this to access sensitive files or execute code outside the intended directory.
src/writer/blueprints.py Line 789 The exception handling code added in lines 779-790 and 797-818 catches all exceptions when trying to report to Sentry, but silently suppresses any errors that might occur during this process with a bare except Exception: pass. This could hide important configuration or integration issues with Sentry. Consider logging these exceptions instead of silently ignoring them.
src/writer/serve.py Line 610 The code imports and uses sentry_sdk without proper validation of user input. In the error handling for blueprint API execution (lines 603-621), the code captures exceptions with Sentry and includes user-provided blueprint_id and branch_id as tags. If these values contain malicious content, they could be used to inject data into monitoring systems. The code should sanitize these values before using them with Sentry.
src/writer/blueprints.py Line 789 Try, Except, Pass detected.
src/writer/serve.py Line 620 Try, Except, Pass detected.
src/writer/blueprints.py Line 817 Try, Except, Pass detected.
src/writer/observability/sentry_adapter.py Line 26 self._org_id: Optional[str] is a built-in function and should be avoided as a variable name. Using language-provided function names as variable names can complicate future development since any attempt to call that specific function within this scope may result in unintended behaviors.
src/writer/observability/base.py Line 34 self._providers: Dict[str, ObservabilityProvider] is a built-in function and should be avoided as a variable name. Using language-provided function names as variable names can complicate future development since any attempt to call that specific function within this scope may result in unintended behaviors.
src/ui/.env Line 3 Committing a .env file to version control is against best practice and can often cause secrets to be committed to a repository even if unintentional. It is acceptable to include a .env.example that does not contain any sensitive data and can be easily copied to a .env as a starting template for necessary environment variables. Consider adding .env to your .gitignore as it should be only added locally to each environment where the code is run.
src/ui/.env Line 2 Committing a .env file to version control is against best practice and can often cause secrets to be committed to a repository even if unintentional. It is acceptable to include a .env.example that does not contain any sensitive data and can be easily copied to a .env as a starting template for necessary environment variables. Consider adding .env to your .gitignore as it should be only added locally to each environment where the code is run.
src/writer/observability/sentry_adapter.py Line 25 self._agent_id: Optional[str] is a built-in function and should be avoided as a variable name. Using language-provided function names as variable names can complicate future development since any attempt to call that specific function within this scope may result in unintended behaviors.
src/ui/.env Line 1 Committing a .env file to version control is against best practice and can often cause secrets to be committed to a repository even if unintentional. It is acceptable to include a .env.example that does not contain any sensitive data and can be easily copied to a .env as a starting template for necessary environment variables. Consider adding .env to your .gitignore as it should be only added locally to each environment where the code is run.
src/ui/src/observability/sentryAdapter.ts Line 641 Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.
src/ui/src/observability/sentryAdapter.ts Line 617 Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.
src/ui/src/observability/sentryAdapter.ts Line 661 Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.
src/ui/src/observability/base.ts Line 108 Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.
src/ui/.env Line 4 Committing a .env file to version control is against best practice and can often cause secrets to be committed to a repository even if unintentional. It is acceptable to include a .env.example that does not contain any sensitive data and can be easily copied to a .env as a starting template for necessary environment variables. Consider adding .env to your .gitignore as it should be only added locally to each environment where the code is run.
src/ui/.env Line 5 Committing a .env file to version control is against best practice and can often cause secrets to be committed to a repository even if unintentional. It is acceptable to include a .env.example that does not contain any sensitive data and can be easily copied to a .env as a starting template for necessary environment variables. Consider adding .env to your .gitignore as it should be only added locally to each environment where the code is run.
src/ui/src/observability/base.ts Line 129 Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use constant values for the format string.
🧰 Analysis tools

⏱️ Latest scan covered changes up to commit 4912adf (latest)

Copy link

@pullrequest pullrequest bot left a comment

Choose a reason for hiding this comment

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

This is quite a large MR, and will require a few passes to review in full. I'll keep you updated as the review progresses.

Image of Graham C Graham C


Reviewed with ❤️ by PullRequest


self._agent_id = os.getenv("WRITER_APP_ID")
self._org_id = os.getenv("WRITER_ORG_ID")
environment = os.getenv(SENTRY_ENVIRONMENT_ENV, "production")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can use WRITER_BASE_URL?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/writer/observability/sentry_adapter.py (2)

149-154: PII leakage concern with URL and query parameters remains unaddressed.

This was flagged in a previous review: full URLs and query parameters may contain tokens, API keys, or user identifiers that would be sent to Sentry unfiltered. The before_send hook sanitizes headers but doesn't touch event.request or query strings.


61-65: Environment detection may misreport test/dev as production.

As noted in a previous review, without SENTRY_ENVIRONMENT set, all environments default to "production". Consider deriving environment from other signals (e.g., WRITER_BASE_URL or a dedicated config) to avoid polluting production error data with test traffic.

src/ui/src/observability/frontendMetrics.ts (1)

98-115: Deduplication mitigates but doesn't resolve the double-counting issue flagged in past reviews.

While the lastTrackedRoute guard prevents consecutive identical routes from being double-counted, the prior review identified that both navigation.ts (lines 79, 89) and SentryAdapter._setupRouteTracking call into route-change tracking for the same navigation events. If these code paths pass different tag values (e.g., pageKey vs. router name/path), deduplication won't catch them. The recommended fix was to choose a single source of truth—either remove manual trackRouteChange calls where hash navigation occurs, or disable the router hook in SentryAdapter—so only one path emits the metric.

Based on past review comments, consolidate to a single tracking source rather than relying solely on deduplication.

🧹 Nitpick comments (6)
src/ui/src/composables/useGlobalErrorHandling.ts (1)

17-24: Inconsistent error type tagging for window errors.

Line 20 uses error.name || "window_error" as the error type, but for generic Error objects, error.name is always "Error", so "window_error" is never used as fallback. This differs from the consistent "unhandled_promise_rejection" used in handleUnhandledRejection. Consider using "window_error" consistently:

 function handleError(event: ErrorEvent): void {
 	try {
 		const error = extractError(event.error || event.message);
-		trackError(error, error.name || "window_error");
+		trackError(error, "window_error");
 	} catch (trackingError) {
 		console.error("Failed to track error:", trackingError);
 	}
 }
src/ui/src/core/index.ts (1)

748-760: Redundant variable assignment.

messageStartTime and interactionStartTime are assigned the same value on consecutive lines. Since interactionStartTime is only used in the callback and messageStartTime is used for the send duration, you could simplify by using a single timestamp:

-		const messageStartTime = performance.now();
-		const interactionStartTime = performance.now();
+		const messageStartTime = performance.now();

 		const wrappedCallback = callback
 			? (response: { ok: boolean; payload?: unknown }) => {
 					if (response?.ok && type !== "keepAlive") {
-						const duration =
-							performance.now() - interactionStartTime;
+						const duration = performance.now() - messageStartTime;
 						trackInteractionDuration(type, duration);
 					}
 					callback(response);
 				}
 			: undefined;
src/writer/observability/__init__.py (2)

19-32: Repeated adapter registration on each get_registry() call.

_register_sentry_adapter() is called every time get_registry() is invoked, which will attempt to re-register the adapter. While ObservabilityRegistry.register() handles duplicates with a warning, this is inefficient and noisy. Consider adding a guard:

+_sentry_registered = False
+
+
 def _register_sentry_adapter(app_path: Optional[str] = None):
+    global _sentry_registered
+    if _sentry_registered:
+        return
     try:
         from writer.observability.sentry_adapter import SentryAdapter
         sentry_adapter = SentryAdapter(app_path=app_path)
         if sentry_adapter.is_enabled():
             _observability_registry.register("sentry", sentry_adapter)
             __all__.append("SentryAdapter")
+        _sentry_registered = True
     except Exception as e:
         logger.debug(f"Sentry adapter not available: {e}")

24-25: Mutating __all__ at runtime may cause duplicates and hinders static analysis.

If _register_sentry_adapter is called multiple times, "SentryAdapter" could be appended repeatedly. Also, static analysis tools won't recognize this dynamic export. Consider checking before appending:

         if sentry_adapter.is_enabled():
             _observability_registry.register("sentry", sentry_adapter)
-            __all__.append("SentryAdapter")
+            if "SentryAdapter" not in __all__:
+                __all__.append("SentryAdapter")
src/writer/observability/sentry_adapter.py (1)

69-77: Rename unused hint parameter to satisfy linter.

The hint parameter is required by Sentry's callback signature but unused here.

-            def before_send(event, hint):
+            def before_send(event, _hint):
                 """Filter sensitive data before sending to Sentry."""
src/ui/src/observability/sentryAdapter.ts (1)

187-192: Sample rates not clamped to valid range.

Unlike the backend adapter (which clamps traces_sample_rate to [0.0, 1.0]), this code uses the parsed values directly. Invalid values (negative or >1.0) could cause unexpected Sentry behavior.

 			const tracesSampleRate = parseFloat(
 				import.meta.env[SENTRY_TRACES_SAMPLE_RATE_ENV] || "1.0",
 			);
+			const clampedTracesSampleRate = Math.max(0, Math.min(1, tracesSampleRate));
 			const replaySampleRate = parseFloat(
 				import.meta.env[SENTRY_REPLAY_SAMPLE_RATE_ENV] || "0.1",
 			);
+			const clampedReplaySampleRate = Math.max(0, Math.min(1, replaySampleRate));

 			const initConfig: SentryInitConfig = {
 				dsn,
 				environment,
-				tracesSampleRate,
+				tracesSampleRate: clampedTracesSampleRate,
 				replay: {
-					sampleRate: replaySampleRate,
+					sampleRate: clampedReplaySampleRate,
 				},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd45c2d and c001690.

📒 Files selected for processing (11)
  • src/ui/src/composables/useBlueprintNodeTools.ts (1 hunks)
  • src/ui/src/composables/useGlobalErrorHandling.ts (1 hunks)
  • src/ui/src/composables/useLogger.ts (1 hunks)
  • src/ui/src/composables/useWriterTracking.ts (2 hunks)
  • src/ui/src/core/index.ts (4 hunks)
  • src/ui/src/main.ts (5 hunks)
  • src/ui/src/observability/base.ts (1 hunks)
  • src/ui/src/observability/frontendMetrics.ts (1 hunks)
  • src/ui/src/observability/sentryAdapter.ts (1 hunks)
  • src/writer/observability/__init__.py (1 hunks)
  • src/writer/observability/sentry_adapter.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/ui/src/composables/useBlueprintNodeTools.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/ui/src/composables/useLogger.ts
  • src/ui/src/main.ts
  • src/ui/src/composables/useWriterTracking.ts
  • src/ui/src/observability/base.ts
🧰 Additional context used
🧬 Code graph analysis (6)
src/ui/src/composables/useGlobalErrorHandling.ts (1)
src/ui/src/observability/frontendMetrics.ts (1)
  • trackError (75-83)
src/ui/src/core/index.ts (1)
src/ui/src/observability/frontendMetrics.ts (4)
  • trackWebSocketLatency (85-96)
  • trackInteractionDuration (117-131)
  • recordDistributionSafely (170-193)
  • incrementMetricSafely (141-159)
src/ui/src/observability/frontendMetrics.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/writer/observability/__init__.py (2)
src/writer/observability/base.py (4)
  • ObservabilityProvider (8-27)
  • ObservabilityRegistry (30-98)
  • is_enabled (25-27)
  • register (37-41)
src/writer/observability/sentry_adapter.py (1)
  • is_enabled (28-30)
src/ui/src/observability/sentryAdapter.ts (3)
src/ui/src/observability/index.ts (2)
  • SentryAdapter (6-6)
  • ObservabilityProvider (4-4)
src/ui/src/observability/base.ts (1)
  • ObservabilityProvider (2-22)
src/ui/src/observability/frontendMetrics.ts (1)
  • trackRouteChange (100-115)
src/writer/observability/sentry_adapter.py (1)
src/ui/src/observability/base.ts (1)
  • ObservabilityProvider (2-22)
🪛 Ruff (0.14.5)
src/writer/observability/__init__.py

26-26: Do not catch blind exception: Exception

(BLE001)

src/writer/observability/sentry_adapter.py

69-69: Unused function argument: hint

(ARG001)


110-110: Consider moving this statement to an else block

(TRY300)


159-159: Consider moving this statement to an else block

(TRY300)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
  • GitHub Check: tests (webkit)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.9)
  • GitHub Check: build (3.12)
  • GitHub Check: tests (chromium)
  • GitHub Check: tests (firefox)
🔇 Additional comments (12)
src/ui/src/composables/useGlobalErrorHandling.ts (1)

35-52: LGTM!

Clean implementation with proper guards for SSR environments and double-initialization prevention. The pattern of storing handler references for proper cleanup is correct.

src/ui/src/core/index.ts (2)

789-816: LGTM!

Good separation of concerns: message send duration (local serialization + send time) vs. interaction duration (round-trip callback). Error metrics are properly tagged.


819-852: LGTM!

Component lifecycle metrics are properly instrumented. Good pattern of capturing component type before deletion and consistent use of incrementMetricSafely with logger.

src/writer/observability/sentry_adapter.py (1)

119-165: LGTM on middleware structure.

The middleware correctly uses push_scope for request-scoped tags and captures exceptions with proper re-raising. The error handling for middleware addition (lines 169-173) appropriately handles the "app already started" case.

src/ui/src/observability/sentryAdapter.ts (2)

108-271: LGTM on initialization flow.

Robust initialization with proper fallbacks for BrowserTracing integration, error handling for dynamic imports, and correct Vue/router integration. The async initialization pattern allows for graceful degradation when Sentry SDK is unavailable.


569-634: LGTM on metric methods.

Consistent pattern across incrementMetric, recordDistribution, and setGauge with proper guards and error handling. The _buildAttributes helper correctly enriches metrics with app metadata.

src/ui/src/observability/frontendMetrics.ts (6)

3-46: LGTM: Well-structured type definitions.

The enums, interfaces, and provider types provide good type safety and clear contracts for metric operations.


75-83: LGTM: Good defensive programming.

The function correctly limits error message length to prevent high cardinality and provides sensible fallbacks for missing error information.


85-96: LGTM: Appropriate metric type for latency.

Using a distribution metric for latency is correct and allows for percentile calculations.


117-131: LGTM: Clean interaction tracking.

The function correctly uses a distribution metric for duration tracking with appropriate units and tags.


133-159: LGTM: Addresses past feedback on wrapping metric calls.

This utility function eliminates the need for inline type checks and repetitive error handling when incrementing metrics, directly addressing the prior review comment about wrapping function calls.

Based on past review comments, this consolidates the common pattern requested.


161-193: LGTM: Consistent safe wrapper pattern.

This function mirrors the incrementMetricSafely pattern, providing consistent error handling and type checking for distribution metrics.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/ui/src/observability/frontendMetrics.ts (3)

26-35: Consider tightening the unit option type to reduce mistakes

RecordDistributionOptions.unit and IncrementMetricOptions.unit are plain string while you already expose MetricUnit as the canonical set. If you don’t expect non-standard units, making these unit?: MetricUnit (or MetricUnit | string if you need extensibility) would catch typos at compile time without changing runtime behavior.


49-84: Navigation Timing v2 usage looks good; consider a lightweight fallback path

Nice move to performance.getEntriesByType("navigation") and using PerformanceNavigationTiming. In environments where window.performance exists but getEntriesByType("navigation") isn’t implemented or returns no entries, loadTime stays null and you silently skip emitting the metric.

If you care about broader browser coverage, you could:

  • Guard performance.getEntriesByType with a typeof === "function" check, and
  • As a fallback, use the legacy window.performance.timing calculation when navigationEntries is empty or the fields are missing.

Example sketch:

-		const navigationEntries = performance.getEntriesByType(
-			"navigation",
-		) as PerformanceNavigationTiming[];
+		const navigationEntries =
+			typeof performance.getEntriesByType === "function"
+				? (performance.getEntriesByType("navigation") as PerformanceNavigationTiming[])
+				: [];
 		if (navigationEntries.length > 0) {
 			const entry = navigationEntries[0];
 			if (entry.loadEventEnd && entry.startTime) {
 				loadTime = entry.loadEventEnd - entry.startTime;
 			} else if (entry.duration) {
 				loadTime = entry.duration;
 			}
 		}
+
+		if (loadTime === null && window.performance.timing) {
+			const timing = window.performance.timing;
+			if (timing.loadEventEnd && timing.navigationStart) {
+				loadTime = timing.loadEventEnd - timing.navigationStart;
+			}
+		}

This keeps the modern path while preserving metrics where only the old API is available.


105-122: Route de-dupe is good; ensure caller passes a normalized route identifier

lastTrackedRoute de-dupes immediate repeats, which should prevent simple double-counting from the same source. However, the route tag is whatever string the caller passes; if that’s a full path with IDs/query params, you can still get high-cardinality metrics.

To keep the metric usable, make sure call sites pass a normalized identifier (e.g., router name or templated path) and consider documenting that expectation in this function’s JSDoc or in the call sites.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c001690 and d113cbd.

📒 Files selected for processing (4)
  • src/ui/src/composables/useBlueprintNodeTools.ts (1 hunks)
  • src/ui/src/composables/useWriterTracking.ts (2 hunks)
  • src/ui/src/core/index.ts (4 hunks)
  • src/ui/src/observability/frontendMetrics.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/ui/src/composables/useBlueprintNodeTools.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/ui/src/composables/useWriterTracking.ts
  • src/ui/src/core/index.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/src/observability/frontendMetrics.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
🔇 Additional comments (2)
src/ui/src/observability/frontendMetrics.ts (2)

140-155: Safe increment wrapper behavior is solid

The incrementMetricSafely helper is a good pattern: it no-ops cleanly when no provider is initialized and isolates provider failures behind a try/catch, preventing observability from breaking the UI. The "incrementMetric" in provider guard keeps you resilient to partial provider implementations.


157-174: Distribution wrapper mirrors increment wrapper correctly

recordDistributionSafely mirrors the increment helper’s structure and error isolation, giving a consistent and safe surface for all metric producers. No changes needed here.

Comment on lines 86 to 94
export function trackError(error: Error, errorType?: string): void {
incrementMetricSafely(MetricName.FrontendErrorsTotal, {
tags: {
error_type: errorType || error.name || "unknown",
error_message: error.message?.substring(0, 100) || "unknown",
},
unit: MetricUnit.None,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid using error_message as a metric tag to prevent cardinality and cost blowups

Tagging metrics with (truncated) error.message will likely create extremely high-cardinality time series, which is problematic for most metric backends (performance, storage, alert noise) and hard to aggregate meaningfully. You already have FrontendErrorsTotal and a useful error_type dimension; the raw message is better sent to logs/traces/Sentry rather than encoded into metric tags.

I’d recommend dropping error_message from tags (or replacing it with a low-cardinality code/bucket), e.g.:

 export function trackError(error: Error, errorType?: string): void {
 	incrementMetricSafely(MetricName.FrontendErrorsTotal, {
 		tags: {
-			error_type: errorType || error.name || "unknown",
-			error_message: error.message?.substring(0, 100) || "unknown",
+			error_type: errorType || error.name || "unknown",
 		},
 		unit: MetricUnit.None,
 	});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function trackError(error: Error, errorType?: string): void {
incrementMetricSafely(MetricName.FrontendErrorsTotal, {
tags: {
error_type: errorType || error.name || "unknown",
error_message: error.message?.substring(0, 100) || "unknown",
},
unit: MetricUnit.None,
});
}
export function trackError(error: Error, errorType?: string): void {
incrementMetricSafely(MetricName.FrontendErrorsTotal, {
tags: {
error_type: errorType || error.name || "unknown",
},
unit: MetricUnit.None,
});
}
🤖 Prompt for AI Agents
In src/ui/src/observability/frontendMetrics.ts around lines 86 to 94, the metric
tags include error_message which can create very high-cardinality time series;
remove the error_message tag from the MetricName.FrontendErrorsTotal call and
only keep a low-cardinality tag such as error_type (errorType || error.name ||
"unknown"); if you need the full message, send error.message to
logs/traces/Sentry instead or map it to a small set of buckets/error codes
before tagging.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/ui/src/observability/sentryAdapter.ts (1)

9-86: Consider importing types directly from @sentry/vue.

The manual interface definitions increase maintenance overhead and may drift from the actual SDK types over time. Since @sentry/vue is already imported as a type (line 5), consider importing the relevant types directly from the SDK.

Note: This concern was previously raised in past review comments.

🧹 Nitpick comments (1)
src/ui/src/observability/sentryAdapter.ts (1)

387-424: Cache _getAppMetadata() result to avoid duplicate calls.

The _getAppMetadata() method is called twice (lines 397 and 413), performing the same complex property access both times. Consider caching the result in a local variable.

Apply this diff:

 private _setInitialMetadata(SentryModule: SentryModule): void {
 	if (!this.initialized || !SentryModule) {
 		return;
 	}

 	try {
+		const appMetadata = this._getAppMetadata();
+
 		if (SentryModule.setTag) {
 			SentryModule.setTag("platform", "frontend");
 			SentryModule.setTag("framework", "writer-framework");

-			const appMetadata = this._getAppMetadata();
 			if (appMetadata.agent_id) {
 				SentryModule.setTag("agent_id", appMetadata.agent_id);
 			}
 			if (appMetadata.organization_id) {
 				SentryModule.setTag(
 					"organization_id",
 					appMetadata.organization_id,
 				);
 			}
 			if (appMetadata.mode) {
 				SentryModule.setTag("mode", appMetadata.mode);
 			}
 		}

 		if (SentryModule.setContext) {
-			const appMetadata = this._getAppMetadata();
 			SentryModule.setContext("application", appMetadata);
 			SentryModule.setContext("runtime", {
 				name: "browser",
 				user_agent: navigator.userAgent,
 				language: navigator.language,
 			});
 		}
 	} catch (e) {
 		console.warn("Failed to set initial Sentry metadata:", e);
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d113cbd and 9624b0d.

📒 Files selected for processing (1)
  • src/ui/src/observability/sentryAdapter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/src/observability/sentryAdapter.ts (3)
src/ui/src/observability/base.ts (1)
  • ObservabilityProvider (2-22)
src/ui/src/core/navigation.ts (1)
  • getParsedHash (25-37)
src/ui/src/observability/frontendMetrics.ts (1)
  • trackRouteChange (107-122)
🔇 Additional comments (1)
src/ui/src/observability/sentryAdapter.ts (1)

268-313: LGTM! Well-structured error capture with proper guards.

The method correctly handles both Error objects and string messages, includes appropriate runtime context, and has proper error handling. The type assertions are reasonable given Sentry's dynamic API.

Comment on lines 104 to 266
async initialize(app?: unknown, router?: unknown): Promise<boolean> {
if (this.initialized) {
return true;
}

if (router) {
this.router = router;
}

try {
const dsn = import.meta.env[SENTRY_DSN_ENV];
if (!dsn) {
console.info(
"Sentry DSN not provided, skipping Sentry initialization",
);
return false;
}

let SentryModule: SentryModule;
try {
const sentryImport = await import(
/* @vite-ignore */ "@sentry/vue"
);
SentryModule = sentryImport as unknown as SentryModule;
} catch (importError) {
console.warn(
"Failed to import @sentry/vue package. Make sure it's installed: npm install @sentry/vue",
importError,
);
return false;
}

const {
init,
captureException,
captureMessage,
setUser,
setContext,
} = SentryModule;

const integrations: unknown[] = [];

if (SentryModule.vueIntegration) {
integrations.push(
SentryModule.vueIntegration({
app: app || undefined,
}),
);
}

let browserTracingIntegration: BrowserTracingIntegration | null =
null;
if (SentryModule.browserTracingIntegration) {
browserTracingIntegration =
SentryModule.browserTracingIntegration;
} else if (SentryModule.BrowserTracing) {
browserTracingIntegration = SentryModule.BrowserTracing;
} else {
try {
const browserModule = (await import(
/* @vite-ignore */ "@sentry/browser"
)) as {
browserTracingIntegration?: BrowserTracingIntegration;
};
if (browserModule.browserTracingIntegration) {
browserTracingIntegration =
browserModule.browserTracingIntegration;
}
} catch {
console.warn(
"BrowserTracing not available. Performance monitoring will be limited.",
);
}
}

const apiBaseUrl =
import.meta.env.VITE_WRITER_BASE_URL ?? window.location.origin;

const tracesSampleRate = parseFloat(
import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
);
const replaySampleRate = parseFloat(
import.meta.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
);

const initConfig: SentryInitConfig = {
dsn,
environment: apiBaseUrl,
tracesSampleRate,
replay: {
sampleRate: replaySampleRate,
},
enableMetrics: true,
};

if (app) {
initConfig.app = app;
this.app = app;
}

if (browserTracingIntegration) {
const tracingOptions: BrowserTracingOptions = {
tracingOrigins: ["localhost", /^\//],
};

if (this.router) {
tracingOptions.router = this.router;
tracingOptions.routeLabel = "path";
}

if (
typeof browserTracingIntegration === "function" &&
!browserTracingIntegration.prototype
) {
const integration =
browserTracingIntegration as BrowserTracingFunction;
integrations.push(integration(tracingOptions));
} else {
const IntegrationClass =
browserTracingIntegration as BrowserTracingClass;
integrations.push(new IntegrationClass(tracingOptions));
}
}

if (integrations.length > 0) {
initConfig.integrations = integrations;
}

init(initConfig);

this.sentry = {
captureException,
captureMessage,
setUser,
setContext,
};

this.sentryModule = SentryModule;
this.metrics = this._getMetrics(SentryModule);

if (this.metrics) {
console.debug("Sentry metrics API initialized");
} else {
console.warn(
"Sentry metrics API not available (requires SDK 10.25.0+)",
);
}

this.initialized = true;

this._setInitialMetadata(SentryModule);
this._setupRouteTracking();

console.info(`Sentry initialized (environment: ${apiBaseUrl})`);
return true;
} catch (error) {
console.warn(
"Failed to load or initialize @sentry/vue package:",
error,
);
return false;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Break down the initialize method into smaller functions.

The initialize method is over 160 lines and handles multiple distinct responsibilities: guard checks, dynamic imports, integration setup, configuration building, SDK initialization, metadata setup, and route tracking. This makes it difficult to understand, test, and maintain.

Consider extracting helper methods:

  • _loadSentryModule(): Handle dynamic import of @sentry/vue
  • _setupIntegrations(SentryModule, app): Build integrations array
  • _createInitConfig(dsn, integrations): Build init configuration
  • _setupBrowserTracing(SentryModule): Extract lines 154-226

This would make the initialize method a high-level orchestrator that's easier to follow.

🤖 Prompt for AI Agents
In src/ui/src/observability/sentryAdapter.ts around lines 104-266, the
initialize method is too large and mixes multiple responsibilities; refactor by
extracting small private helpers: implement _loadSentryModule() to perform the
dynamic import and return the typed module or null,
_setupIntegrations(SentryModule, app?, router?) to assemble and return the
integrations array (including vue integration and browser tracing creation),
_setupBrowserTracing(SentryModule, app?, router?) to encapsulate the browser
tracing selection and instantiation logic (formerly lines ~154-226), and
_createInitConfig(dsn, integrations, app?) to build and return the
SentryInitConfig object; update initialize to call these helpers in sequence,
handle their return values/early exits, set
this.sentry/this.metrics/this.initialized and call
_setInitialMetadata/_setupRouteTracking as before so initialize becomes a
high-level orchestrator.

Comment on lines +182 to +187
const tracesSampleRate = parseFloat(
import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
);
const replaySampleRate = parseFloat(
import.meta.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate parsed sample rates to prevent NaN.

parseFloat on environment variables without validation can produce NaN if the values are invalid, which would be passed to Sentry configuration. Consider adding validation or using a helper that ensures valid numeric values.

Apply this diff to add validation:

 const tracesSampleRate = parseFloat(
 	import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
 );
+if (isNaN(tracesSampleRate)) {
+	console.warn("Invalid VITE_SENTRY_TRACES_SAMPLE_RATE, defaulting to 0.1");
+	tracesSampleRate = 0.1;
+}
 const replaySampleRate = parseFloat(
 	import.meta.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
 );
+if (isNaN(replaySampleRate)) {
+	console.warn("Invalid VITE_SENTRY_REPLAY_SAMPLE_RATE, defaulting to 0.1");
+	replaySampleRate = 0.1;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tracesSampleRate = parseFloat(
import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
);
const replaySampleRate = parseFloat(
import.meta.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
);
const tracesSampleRate = (() => {
const value = parseFloat(
import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
);
if (isNaN(value)) {
console.warn("Invalid VITE_SENTRY_TRACES_SAMPLE_RATE, defaulting to 0.1");
return 0.1;
}
return value;
})();
const replaySampleRate = (() => {
const value = parseFloat(
import.meta.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
);
if (isNaN(value)) {
console.warn("Invalid VITE_SENTRY_REPLAY_SAMPLE_RATE, defaulting to 0.1");
return 0.1;
}
return value;
})();
🤖 Prompt for AI Agents
In src/ui/src/observability/sentryAdapter.ts around lines 182 to 187, the code
parses sample-rate env vars with parseFloat but doesn’t validate the result;
update the parsing to validate and sanitize the values so NaN or out-of-range
numbers aren’t passed to Sentry: parse the env string, check Number.isFinite or
!Number.isNaN, default to a safe fallback (e.g., 0.1) when invalid, and clamp
the final value to the 0..1 range before using it for tracesSampleRate and
replaySampleRate.

Comment on lines 189 to 197
const initConfig: SentryInitConfig = {
dsn,
environment: apiBaseUrl,
tracesSampleRate,
replay: {
sampleRate: replaySampleRate,
},
enableMetrics: true,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use semantic environment name instead of API base URL.

Setting environment to apiBaseUrl (e.g., "https://api.example.com") makes it difficult to filter and group errors in Sentry by deployment environment. Sentry's environment field should use semantic values like "production", "staging", or "development".

Consider using a dedicated environment variable:

 const initConfig: SentryInitConfig = {
 	dsn,
-	environment: apiBaseUrl,
+	environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || "production",
 	tracesSampleRate,
 	replay: {
 		sampleRate: replaySampleRate,
 	},
 	enableMetrics: true,
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const initConfig: SentryInitConfig = {
dsn,
environment: apiBaseUrl,
tracesSampleRate,
replay: {
sampleRate: replaySampleRate,
},
enableMetrics: true,
};
const initConfig: SentryInitConfig = {
dsn,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || "production",
tracesSampleRate,
replay: {
sampleRate: replaySampleRate,
},
enableMetrics: true,
};
🤖 Prompt for AI Agents
In src/ui/src/observability/sentryAdapter.ts around lines 189 to 197, the Sentry
init is setting environment to apiBaseUrl (a URL) which is not semantic; change
it to use a semantic environment value (e.g. SENTRY_ENV or NODE_ENV mapped to
"production" | "staging" | "development") instead of the API URL. Read a
dedicated env var (SENTRY_ENV) with a safe fallback (e.g. map NODE_ENV ->
semantic value), validate it is one of the expected strings, and pass that into
the initConfig.environment field so Sentry receives a deployment environment
like "production" rather than an API URL.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/ui/src/observability/frontendMetrics.ts (1)

86-93: Remove error_message tag to prevent high-cardinality metric explosion.

This was flagged in a previous review but appears unaddressed. The error_message tag (even truncated to 100 chars) creates extremely high-cardinality time series that degrade metric backend performance and are difficult to aggregate. The error details are better captured via captureException below.

 export function trackError(error: Error, errorType?: string): void {
 	incrementMetricSafely(MetricName.FrontendErrorsTotal, {
 		tags: {
 			error_type: errorType || error.name || "unknown",
-			error_message: error.message?.substring(0, 100) || "unknown",
 		},
 		unit: MetricUnit.None,
 	});
🧹 Nitpick comments (7)
src/ui/vite.config.ts (1)

17-23: Consider lowering default trace sample rate for production.

The default VITE_SENTRY_TRACES_SAMPLE_RATE of "1.0" means 100% of transactions will be traced, which can be expensive in production (both in terms of Sentry quota and performance overhead). A more typical production default is 0.1-0.2 (10-20%).

The replay sample rate default of 0.1 is reasonable.

 		VITE_SENTRY_DSN: JSON.stringify(process.env.VITE_SENTRY_DSN || ""),
 		VITE_SENTRY_TRACES_SAMPLE_RATE: JSON.stringify(
-			process.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "1.0",
+			process.env.VITE_SENTRY_TRACES_SAMPLE_RATE || "0.1",
 		),
 		VITE_SENTRY_REPLAY_SAMPLE_RATE: JSON.stringify(
 			process.env.VITE_SENTRY_REPLAY_SAMPLE_RATE || "0.1",
 		),
src/writer/observability/sentry_adapter.py (1)

70-72: Consider lowering default trace sample rate for production.

Similar to the frontend, the default SENTRY_TRACES_SAMPLE_RATE of "1.0" means 100% sampling, which can be costly. Consider using a lower default like 0.1.

             environment = os.getenv(SENTRY_ENVIRONMENT_ENV, "production")
-            traces_sample_rate = float(os.getenv(SENTRY_TRACES_SAMPLE_RATE_ENV, "1.0"))
+            traces_sample_rate = float(os.getenv(SENTRY_TRACES_SAMPLE_RATE_ENV, "0.1"))
             traces_sample_rate = max(0.0, min(1.0, traces_sample_rate))
src/ui/src/observability/frontendMetrics.ts (1)

119-134: Consider normalizing route tags to prevent cardinality issues.

If route contains dynamic path segments (e.g., /users/123), each unique value creates a new time series. Consider normalizing to route templates (e.g., /users/:id) or using route names instead of full paths.

src/writer/app_runner.py (1)

502-508: Log message assumes Sentry but could be any provider.

The initialize_provider() method initializes whichever provider is enabled (determined by OBSERVABILITY_PROVIDER env var or first enabled provider). The log messages mentioning "Sentry" are misleading if a different provider is configured.

         try:
             from writer.observability import get_registry
             observability_registry = get_registry(app_path=self.app_path)
             if observability_registry.initialize_provider():
-                self.logger.info("Sentry initialized in app process")
+                provider = observability_registry.get_initialized_provider()
+                provider_name = provider.get_name() if provider else "unknown"
+                self.logger.info(f"Observability provider '{provider_name}' initialized in app process")
         except Exception as e:
-            self.logger.warning(f"Failed to initialize Sentry in app process: {e}")
+            self.logger.warning(f"Failed to initialize observability provider in app process: {e}")
src/writer/blueprints.py (2)

779-791: Consider importing sentry_sdk at module level.

Importing sentry_sdk inside the exception handler adds overhead on every error. Consider importing at the top of the module with a fallback pattern:

+try:
+    import sentry_sdk
+    HAS_SENTRY = True
+except ImportError:
+    HAS_SENTRY = False
+
 logger = logging.getLogger(__name__)

Then simplify the handler:

-                    try:
-                        import sentry_sdk
+                    if HAS_SENTRY:
+                        try:
-                        with sentry_sdk.push_scope() as scope:
-                            scope.set_tag("source", "blueprint_execution")
-                            scope.set_tag("error_type", "BlueprintExecutionError")
-                            scope.set_context("blueprint", {
-                                "run_id": self.run_id,
-                                "title": self.status_logger.title,
-                            })
-                            sentry_sdk.capture_exception(e)
-                    except Exception:
-                        pass
+                            with sentry_sdk.push_scope() as scope:
+                                scope.set_tag("source", "blueprint_execution")
+                                scope.set_tag("error_type", "BlueprintExecutionError")
+                                scope.set_context("blueprint", {
+                                    "run_id": self.run_id,
+                                    "title": self.status_logger.title,
+                                })
+                                sentry_sdk.capture_exception(e)
+                        except Exception:
+                            logger.debug("Failed to report exception to Sentry", exc_info=True)

This avoids repeated imports and adds debug logging for troubleshooting.


797-819: Consider extracting Sentry reporting to a helper function.

This error reporting block is very similar to the one at lines 779-791, creating maintenance burden. Consider extracting a helper:

def _report_to_sentry(exception: Exception, run_id: str, title: str, **extra_tags):
    """Report exception to Sentry if available."""
    if not HAS_SENTRY:
        return
    try:
        with sentry_sdk.push_scope() as scope:
            scope.set_tag("source", "blueprint_execution")
            scope.set_tag("error_type", type(exception).__name__)
            for key, value in extra_tags.items():
                scope.set_tag(key, value)
            scope.set_context("blueprint", {
                "run_id": run_id,
                "title": title,
            })
            sentry_sdk.capture_exception(exception)
    except Exception:
        logger.debug("Failed to report exception to Sentry", exc_info=True)

Then use it in both handlers:

except BlueprintExecutionError as e:
    _report_to_sentry(e, self.run_id, self.status_logger.title)
    # ... existing error handling

except BaseException as e:
    _report_to_sentry(e, self.run_id, self.status_logger.title,
                     platform="backend", component="backend", 
                     layer="server")
    # ... existing error handling
src/writer/serve.py (1)

603-622: Apply the same optimization suggested for blueprints.py.

This Sentry reporting block has the same efficiency issue as in src/writer/blueprints.py (lines 779-791, 797-819). For consistency across the codebase, consider:

  1. Importing sentry_sdk at module level with a HAS_SENTRY flag
  2. Using the same helper function pattern if you implement it in blueprints.py

This ensures consistent error reporting behavior across blueprint execution contexts.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9624b0d and 4912adf.

📒 Files selected for processing (8)
  • src/ui/src/observability/frontendMetrics.ts (1 hunks)
  • src/ui/src/observability/sentryAdapter.ts (1 hunks)
  • src/ui/vite.config.ts (1 hunks)
  • src/writer/app_runner.py (1 hunks)
  • src/writer/blueprints.py (2 hunks)
  • src/writer/core.py (1 hunks)
  • src/writer/observability/sentry_adapter.py (1 hunks)
  • src/writer/serve.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/src/observability/sentryAdapter.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-22T11:08:20.053Z
Learnt from: VladKrasny
Repo: writer/writer-framework PR: 1100
File: src/ui/src/composables/useFilesEncoder/useFilesEncoder.ts:14-22
Timestamp: 2025-08-22T11:08:20.053Z
Learning: The writer-framework project does not require SSR (Server-Side Rendering) support, so using `window` object is acceptable in the codebase.

Applied to files:

  • src/ui/vite.config.ts
📚 Learning: 2025-06-16T15:34:16.165Z
Learnt from: ramedina86
Repo: writer/writer-framework PR: 935
File: src/writer/blueprints.py:210-212
Timestamp: 2025-06-16T15:34:16.165Z
Learning: In src/writer/blueprints.py, execution environments passed to run_branch and run_blueprint methods are intentionally mutated during execution. The environments need to be created beforehand and should not be deep-copied, as the mutation is part of the intended workflow design.

Applied to files:

  • src/writer/blueprints.py
🧬 Code graph analysis (5)
src/ui/src/observability/frontendMetrics.ts (1)
src/ui/src/observability/index.ts (1)
  • observabilityRegistry (8-8)
src/writer/observability/sentry_adapter.py (3)
src/ui/src/observability/base.ts (1)
  • ObservabilityProvider (2-22)
src/writer/observability/base.py (4)
  • ObservabilityProvider (8-27)
  • is_enabled (25-27)
  • initialize (12-14)
  • instrument_fastapi_app (17-19)
src/ui/src/observability/sentryAdapter.ts (2)
  • SENTRY_DSN_ENV (7-7)
  • initialize (104-270)
src/writer/serve.py (2)
src/writer/observability/__init__.py (1)
  • get_registry (30-32)
src/writer/observability/base.py (2)
  • initialize_provider (51-84)
  • instrument_app (86-94)
src/writer/app_runner.py (2)
src/writer/observability/__init__.py (1)
  • get_registry (30-32)
src/writer/observability/base.py (1)
  • initialize_provider (51-84)
src/writer/blueprints.py (2)
src/writer/journal.py (1)
  • save (67-72)
src/writer/keyvalue_storage.py (1)
  • save (49-55)
🪛 Ruff (0.14.5)
src/writer/observability/sentry_adapter.py

76-76: Unused function argument: hint

(ARG001)


124-124: Consider moving this statement to an else block

(TRY300)


170-170: Consider moving this statement to an else block

(TRY300)

src/writer/serve.py

172-172: Do not catch blind exception: Exception

(BLE001)


620-621: try-except-pass detected, consider logging the exception

(S110)


620-620: Do not catch blind exception: Exception

(BLE001)

src/writer/app_runner.py

507-507: Do not catch blind exception: Exception

(BLE001)

src/writer/blueprints.py

789-790: try-except-pass detected, consider logging the exception

(S110)


789-789: Do not catch blind exception: Exception

(BLE001)


795-795: Use raise without specifying exception name

Remove exception name

(TRY201)


817-818: try-except-pass detected, consider logging the exception

(S110)


817-817: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (2)
src/writer/blueprints.py (1)

20-20: LGTM!

Module-level logger initialization follows Python best practices and supports the observability enhancements.

src/writer/serve.py (1)

167-173: LGTM!

The observability initialization is properly defensive—failures are logged but don't prevent the app from starting. The broad exception catch is appropriate here for an optional feature.

Comment on lines +1892 to +1917
try:
import sentry_sdk
with sentry_sdk.push_scope() as scope:
scope.set_tag("platform", "backend")
scope.set_tag("component", "backend")
scope.set_tag("layer", "server")
scope.set_tag("source", "event_handler")
scope.set_tag("event_type", ev.type)
scope.set_context("event", {
"type": ev.type,
"handler": ev.handler,
"is_safe": ev.isSafe,
})
scope.set_context("component", {
"type": "backend",
"platform": "backend",
"layer": "server",
"source": "event_handler",
})
event_id = sentry_sdk.capture_exception(e)
if event_id:
logging.info(f"Error sent to Sentry (event_id: {event_id}, event_type: {ev.type})")
except ImportError:
logging.warning("Sentry SDK not available - error will not be sent to Sentry")
except Exception as sentry_error:
logging.error(f"Failed to send error to Sentry: {sentry_error}", exc_info=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Direct sentry_sdk import bypasses the observability abstraction layer.

This code directly imports and uses sentry_sdk, bypassing the ObservabilityRegistry and SentryAdapter abstraction established elsewhere in this PR. This creates several issues:

  1. If SENTRY_ENABLED=false, this still attempts to use Sentry
  2. The tag/context setup duplicates what SentryAdapter.initialize() already configures globally
  3. Inconsistent with app_runner.py which uses the registry pattern

Consider using the registry's initialized provider instead:

         except BaseException as e:
             try:
-                import sentry_sdk
-                with sentry_sdk.push_scope() as scope:
-                    scope.set_tag("platform", "backend")
-                    scope.set_tag("component", "backend")
-                    scope.set_tag("layer", "server")
-                    scope.set_tag("source", "event_handler")
-                    scope.set_tag("event_type", ev.type)
-                    scope.set_context("event", {
-                        "type": ev.type,
-                        "handler": ev.handler,
-                        "is_safe": ev.isSafe,
-                    })
-                    scope.set_context("component", {
-                        "type": "backend",
-                        "platform": "backend",
-                        "layer": "server",
-                        "source": "event_handler",
-                    })
-                    event_id = sentry_sdk.capture_exception(e)
-                    if event_id:
-                        logging.info(f"Error sent to Sentry (event_id: {event_id}, event_type: {ev.type})")
-            except ImportError:
-                logging.warning("Sentry SDK not available - error will not be sent to Sentry")
-            except Exception as sentry_error:
-                logging.error(f"Failed to send error to Sentry: {sentry_error}", exc_info=True)
+                from writer.observability import get_registry
+                registry = get_registry()
+                provider = registry.get_initialized_provider()
+                if provider:
+                    import sentry_sdk
+                    with sentry_sdk.push_scope() as scope:
+                        scope.set_tag("source", "event_handler")
+                        scope.set_tag("event_type", ev.type)
+                        scope.set_context("event", {
+                            "type": ev.type,
+                            "handler": ev.handler,
+                            "is_safe": ev.isSafe,
+                        })
+                        sentry_sdk.capture_exception(e)
+            except Exception as obs_error:
+                logging.debug(f"Could not report error to observability provider: {obs_error}")
🤖 Prompt for AI Agents
In src/writer/core.py around lines 1892-1917, replace the direct import/use of
sentry_sdk with the observability abstraction: obtain the initialized Sentry
provider from ObservabilityRegistry (or the SentryAdapter) and check that SENTRY
is enabled/that a provider instance exists before calling it; use the provider's
capture_exception API (or its scoped capture helper) to record the exception and
attach event metadata (event_type, handler, is_safe) via the provider's
supported parameters (e.g., extra/context) rather than re-setting global
tags/contexts, and preserve the ImportError/exception handling by logging a
warning if no provider is available and logging errors if sending fails.

Comment on lines +97 to +120
with sentry_sdk.configure_scope() as scope:
scope.set_tag("platform", "backend")
scope.set_tag("component", "backend")
scope.set_tag("layer", "server")
scope.set_tag("framework", "writer-framework")
if metadata["agent_id"]:
scope.set_tag("agent_id", metadata["agent_id"])
if metadata["organization_id"]:
scope.set_tag("organization_id", metadata["organization_id"])
if metadata.get("app_path"):
scope.set_tag("app_path", metadata["app_path"])
scope.set_context("application", {
"path": metadata["app_path"],
"name": metadata.get("app_name"),
})
scope.set_context("runtime", {
"name": "python",
"version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
})
scope.set_context("component", {
"type": "backend",
"platform": "backend",
"layer": "server",
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify configure_scope is not deprecated in your Sentry SDK version.

The configure_scope() context manager has been deprecated in Sentry SDK 2.x in favor of sentry_sdk.get_current_scope() or scope isolation patterns. Verify your SDK version and consider updating if you're on 2.x or later.


🌐 Web query:

Is sentry_sdk configure_scope deprecated in Python SDK 2.0?

💡 Result:

Short answer: Yes — sentry_sdk.configure_scope is deprecated in sentry-python 2.0 and marked for removal. Use the new scope APIs (get_current_scope / get_isolation_scope, new_scope, isolation_scope, or the Scope methods) instead. [1][2]

Sources:

  • Migration guide (1.x → 2.x): explains configure_scope/push_scope deprecation and replacements. [1]
  • sentry-python API docs: configure_scope marked "Deprecated since version 2.0.0". [2]

Update to non-deprecated Sentry scope APIs.

The configure_scope() context manager is deprecated since version 2.0.0 in sentry-python and marked for removal. Replace with get_current_scope(), get_isolation_scope(), new_scope, isolation_scope, or the Scope methods instead (lines 97–120).

🤖 Prompt for AI Agents
In src/writer/observability/sentry_adapter.py around lines 97–120, the code uses
the deprecated sentry_sdk.configure_scope() context manager; replace it with a
non-deprecated API such as sentry_sdk.push_scope() (to create an isolated
temporary scope) or
sentry_sdk.get_current_scope()/sentry_sdk.get_isolation_scope()/sentry_sdk.new_scope()
depending on desired isolation. Specifically, swap the configure_scope() usage
for sentry_sdk.push_scope() as the context manager (or obtain a scope via
get_current_scope() if you intend to mutate the global scope), then apply the
same scope.set_tag and scope.set_context calls to that scope so behavior remains
identical without using the deprecated API.

@pullrequest
Copy link

pullrequest bot commented Nov 26, 2025

PullRequest reviewed the updates made to #1212 up until the latest commit (4912adf). No further issues were found.

Reviewed by:

Image of Graham C Graham C

@pullrequest
Copy link

pullrequest bot commented Nov 28, 2025

Graham C has submitted feedback.

Image of Graham C Graham C


Reviewed with ❤️ by PullRequest

@@ -0,0 +1,5 @@
VITE_SENTRY_DSN=https://6c13c3260114c5b1ceb3b6ae58d0cdb3@o1026471.ingest.us.sentry.io/4510398008393728
Copy link

Choose a reason for hiding this comment

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

The Sentry DSN (Data Source Name) is hardcoded and committed to version control. This credential should be treated as a secret. Anyone with repository access can view this DSN, and it remains in git history even if removed in future commits. While Sentry DSNs are designed for client-side use (write-only), exposing them allows malicious actors to send false error reports to your project, potentially causing rate limit issues and polluting your error tracking data.

Committing environment-specific configuration files violates the principle of separating configuration from code. This DSN appears to be for production, which means developers might accidentally send development errors to production Sentry.

Remediation:

Add .env to .gitignore and create .env.example with placeholder values:

VITE_SENTRY_DSN=your_sentry_dsn_here
VITE_SENTRY_ENABLED=true

Configure actual values through your deployment platform's environment variables (Vercel, Netlify, AWS, etc.). Remove .env from git history using tools like BFG Repo-Cleaner. Consider rotating the exposed DSN in Sentry settings.

🔺 Vulnerability (Error)

Image of Graham C Graham C

return error;
}
if (typeof error === "string") {
return new Error(error);
Copy link

Choose a reason for hiding this comment

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

The extractError function converts error strings to Error objects without validating or limiting message length. According to OWASP input validation guidelines, all external input should have maximum length constraints to prevent resource exhaustion. Excessively long error messages can cause memory issues, exceed API payload limits when sent to Sentry, and consume unnecessary storage in logs.

Error messages might include stack traces, large JSON payloads, or extensive debugging information. Without length limits, this creates a DoS vector where triggered errors with arbitrarily large messages could exhaust resources.

Remediation:

Implement maximum length validation:

const MAX_ERROR_LENGTH = 1500;

function extractError(error: unknown): Error {
    if (error instanceof Error) {
        if (error.message.length > MAX_ERROR_LENGTH) {
            error.message = error.message.substring(0, MAX_ERROR_LENGTH) + '... (truncated)';
        }
        return error;
    }
    
    let msg = typeof error === "string" ? error : String(error || "Unknown error");
    if (msg.length > MAX_ERROR_LENGTH) {
        msg = msg.substring(0, MAX_ERROR_LENGTH) + '... (truncated)';
    }
    return new Error(msg);
}
🔸 Vulnerability (Warning)

Image of Graham C Graham C

: new Error(String(args[0]));
provider.captureException(error, {
source: "logger",
component: "useLogger",
Copy link

Choose a reason for hiding this comment

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

The error method passes all additional arguments to the observability provider as context without sanitization. Developers often include user objects, request payloads, or application state when logging errors. If any of this context contains passwords, API keys, tokens, PII, or other sensitive data, it will be sent to Sentry and accessible to all team members.

This violates data protection regulations and creates a security liability. Once sensitive data enters Sentry, it may be retained for extended periods with less stringent access controls than production databases.

Remediation:

Sanitize arguments before sending to observability:

const SENSITIVE = /password|secret|token|key|auth|api[-_]?key/i;

function sanitizeArgs(args: any[]): any[] {
    return args.slice(0, 5).map(arg => {
        if (typeof arg === 'string') {
            return arg.substring(0, 500);
        }
        if (arg && typeof arg === 'object') {
            const clean: any = {};
            for (const [k, v] of Object.entries(arg).slice(0, 20)) {
                clean[k] = SENSITIVE.test(k) ? '[REDACTED]' : v;
            }
            return clean;
        }
        return arg;
    });
}

Apply the same sanitization to the warn method at line 21.

🔸 Vulnerability (Warning)

Image of Graham C Graham C


const initConfig: SentryInitConfig = {
dsn,
environment: "production",
Copy link

Choose a reason for hiding this comment

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

The environment is hardcoded as "production" instead of using the VITE_SENTRY_ENVIRONMENT variable defined in .env. This causes all errors from development, staging, and production to be tagged as production errors in Sentry, making it difficult to identify actual production issues. Development errors pollute production reports, alerting rules fire incorrectly, and metrics about production error rates become inaccurate.

The .env file defines VITE_SENTRY_ENVIRONMENT=production, indicating environment-specific configuration was intended but not implemented.

Remediation:

Use the environment variable:

const initConfig: SentryInitConfig = {
    dsn,
    environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || 
        (import.meta.env.DEV ? "development" : "production"),
    tracesSampleRate,
    replay: { sampleRate: replaySampleRate },
    enableMetrics: true,
};

Ensure your build pipeline sets VITE_SENTRY_ENVIRONMENT appropriately for each deployment environment. Verify in Sentry that events are correctly tagged after this change.

🔺 Bug (Error)

Image of Graham C Graham C

try {
const runtimeContext: RuntimeContext = {
url: window.location.href,
userAgent: navigator.userAgent,
Copy link

Choose a reason for hiding this comment

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

The context object is spread into Sentry's extra field without validating structure, size, or depth. User-provided or application-generated context could be arbitrarily deep or large, causing stack overflow during serialization, exceeding Sentry's payload limits (typically 200KB), or consuming excessive memory. This creates a DoS vector where errors with extremely large context objects could exhaust resources.

Remediation:

Implement validation for context objects:

function sanitizeContext(obj: any, depth = 0): any {
    if (depth > 5) return '[Max depth exceeded]';
    if (!obj || typeof obj !== 'object') return obj;
    
    if (Array.isArray(obj)) {
        return obj.slice(0, 100).map(item => sanitizeContext(item, depth + 1));
    }
    
    const result: any = {};
    for (const [key, value] of Object.entries(obj).slice(0, 50)) {
        result[key] = sanitizeContext(value, depth + 1);
    }
    return result;
}

// In captureException:
const sanitized = sanitizeContext(context);
extra: { ...sanitized, url: window.location.href }

Consider checking total JSON size and truncating if needed.

🔺 Vulnerability (Error)

Image of Graham C Graham C

"title": self.status_logger.title,
})
sentry_sdk.capture_exception(e)
except Exception:
Copy link

Choose a reason for hiding this comment

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

The Sentry error reporting is wrapped in a bare except Exception: pass block that silently suppresses all errors during Sentry integration. While preventing Sentry issues from breaking error handling is correct, silent suppression makes it impossible to detect Sentry configuration problems, initialization failures, or network issues. If Sentry is failing, developers have no indication that errors aren't being tracked, defeating the purpose of error tracking.

Remediation:

Replace silent suppression with proper logging:

except BlueprintExecutionError as e:
    try:
        import sentry_sdk
        with sentry_sdk.push_scope() as scope:
            scope.set_tag("source", "blueprint_execution")
            scope.set_context("blueprint", {
                "run_id": self.run_id,
                "title": self.status_logger.title,
            })
            sentry_sdk.capture_exception(e)
    except ImportError:
        logger.debug("Sentry SDK not available")
    except Exception as sentry_error:
        logger.warning(f"Failed to report error to Sentry: {sentry_error}", exc_info=True)
    
    self._cancel_all_jobs()
    # ... rest of error handling

Apply the same pattern to the BaseException handler at line 818.

🔺 Vulnerability (Error)

Image of Graham C Graham C

scope.set_tag("layer", "server")
scope.set_tag("source", "event_handler")
scope.set_tag("event_type", ev.type)
scope.set_context("event", {
Copy link

Choose a reason for hiding this comment

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

The event type (ev.type) is used directly as a Sentry tag without validation. This value originates from frontend messages and user interactions, making it user-controlled input. Using unvalidated user input in observability tags can cause multiple issues: excessively long strings may hit Sentry's tag limits, special characters could interfere with Sentry's query system, and malicious values could pollute your error tracking.

According to OWASP input validation guidelines, all external input must be validated for type, length, format, and range, even when used for logging or observability.

Remediation:

Validate and sanitize event types before using in tags:

import re

def sanitize_tag(value: str, max_len: int = 100) -> str:
    if not isinstance(value, str):
        value = str(value)
    # Allow only alphanumeric, underscore, hyphen, dot
    sanitized = re.sub(r'[^a-zA-Z0-9_.-]', '_', value)
    return sanitized[:max_len]

# Then use:
scope.set_tag("event_type", sanitize_tag(ev.type))

Apply similar sanitization to ev.handler and review the event context (lines 1903-1906) to ensure no sensitive data is included.

🔺 Vulnerability (Error)

Image of Graham C Graham C

sensitive_keys = ["authorization", "cookie", "x-api-key"]
for key in sensitive_keys:
headers.pop(key.lower(), None)
return event
Copy link

Choose a reason for hiding this comment

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

The header filtering uses case-sensitive lookup (headers.pop(key.lower(), None)) which only works if header keys are already lowercase. HTTP headers are case-insensitive per RFC 7230, meaning "Authorization", "AUTHORIZATION", and "authorization" are equivalent. If the headers dictionary preserves original casing, filtering will fail, potentially exposing authentication tokens and cookies to Sentry.

This is a critical security issue because authorization headers and cookies contain credentials for impersonating users. The limited scope (only 3 headers) also misses other common authentication headers like "X-Auth-Token" or "X-Session-ID".

Remediation:

Normalize header keys to lowercase before filtering:

def before_send(event, hint):
    if "request" in event.get("contexts", {}):
        headers = event["contexts"]["request"].get("headers", {})
        normalized = {k.lower(): v for k, v in headers.items()}
        
        sensitive = ["authorization", "cookie", "x-api-key", 
                    "x-auth-token", "x-session-id", "x-csrf-token"]
        for h in sensitive:
            normalized.pop(h, None)
        
        event["contexts"]["request"]["headers"] = normalized
    return event
🔺 Bug (Error)

Image of Graham C Graham C

"method": request.method,
"url": str(request.url),
"path": request.url.path,
"query_params": dict(request.query_params),
Copy link

Choose a reason for hiding this comment

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

Request query parameters are captured and sent to Sentry without filtering sensitive information. Query parameters frequently contain sensitive data like API keys, access tokens, session identifiers, password reset tokens, or PII. This exposes confidential data to anyone with Sentry access and potentially violates data protection regulations (GDPR, CCPA, HIPAA).

The before_send callback filters sensitive headers but doesn't address query parameters, creating an inconsistent security posture. Future features might add sensitive data to URLs that would be automatically logged.

Remediation:

Implement parameter filtering before adding to context:

SENSITIVE_PARAMS = ['token', 'key', 'secret', 'password', 'api_key', 'auth', 'session']

def filter_params(params):
    filtered = {}
    for k, v in params.items():
        if any(p in k.lower() for p in SENSITIVE_PARAMS):
            filtered[k] = '[FILTERED]'
        else:
            filtered[k] = v
    return filtered

# In middleware:
"query_params": filter_params(dict(request.query_params))

Consider whether query parameters need to be captured at all. The URL path alone is often sufficient for debugging.

🔺 Privacy/Compliance (Error)

Image of Graham C Graham C

scope.set_tag("component", "backend")
scope.set_tag("layer", "server")
scope.set_tag("source", "blueprint_api_endpoint")
scope.set_tag("blueprint_id", blueprint_id)
Copy link

Choose a reason for hiding this comment

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

The blueprint_id from URL path parameters is used directly as a Sentry tag without validation. URL parameters are user-controlled input that could contain specially crafted values to inject data into your monitoring system. Malicious actors could send requests with extremely long IDs, special characters that interfere with Sentry's indexing, or patterns designed to pollute error tracking.

The OWASP Input Validation Cheat Sheet emphasizes that all external input must be validated at trust boundaries, regardless of intended use.

Remediation:

Validate and sanitize IDs before using as tags:

import re

def sanitize_id(value: str, max_len: int = 64) -> str:
    if not value:
        return "unknown"
    # Remove non-alphanumeric except hyphen/underscore
    sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', str(value))
    return sanitized[:max_len] if sanitized else "invalid"

# In error handler:
scope.set_tag("blueprint_id", sanitize_id(blueprint_id))
if branch_id:
    scope.set_tag("branch_id", sanitize_id(branch_id))

If blueprint IDs should be UUIDs, validate explicitly against UUID format. Consider whether these IDs belong in tags or would be better in event context.

🔸 Vulnerability (Warning)

Image of Graham C Graham C

@pullrequest
Copy link

pullrequest bot commented Dec 12, 2025

Due to inactivity, PullRequest has cancelled this review job. You can reactivate the code review job from the PullRequest dashboard.

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.

5 participants