Skip to content

Feature/revenue 2#237

Merged
lindesvard merged 14 commits into
mainfrom
feature/revenue-2
Nov 19, 2025
Merged

Feature/revenue 2#237
lindesvard merged 14 commits into
mainfrom
feature/revenue-2

Conversation

@lindesvard

@lindesvard lindesvard commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • End-to-end revenue tracking: frontend/backend/webhook support, device-id fetching endpoint, SDK methods for revenue, pendingRevenue, flushRevenue, and clearRevenue.
    • New FlowStep and ProjectChart components for timeline and charting.
  • Documentation

    • Added a comprehensive "Revenue tracking" guide with flows and code samples.
  • UI Improvements

    • Currency-aware metrics, revenue visuals in charts/tables, responsive table priorities, and revenue column/widgets added.

@vercel

vercel Bot commented Nov 12, 2025

Copy link
Copy Markdown

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

Project Deployment Preview Comments Updated (UTC)
openpanel-public Error Error Nov 19, 2025 1:08pm

@coderabbitai

coderabbitai Bot commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds end-to-end revenue tracking: DB schema and migrations, session-level revenue aggregation, SDK/web methods for revenue and device-id handling, API endpoint and validation, worker/session changes to carry revenue, overview/chart UI updates to surface revenue, docs and small frontend UX/responsive adjustments.

Changes

Cohort / File(s) Summary
SDK & Web client
packages/sdks/sdk/src/index.ts, packages/sdks/web/src/index.ts, packages/sdks/web/src/types.d.ts
New revenue() and fetchDeviceId() on core SDK; web SDK adds pendingRevenue, flushRevenue, clearRevenue, sessionStorage persistence; types updated to expose revenue-related methods.
API: track & device-id
apps/api/src/controllers/track.controller.ts, apps/api/src/routes/track.router.ts, apps/api/src/controllers/event.controller.ts, apps/api/src/routes/track.router.ts
Added GET /track/device-id handler, fetchDeviceId(); track flow reads override device id from properties; UA handling made optional; route registered.
Backend validation
apps/api/src/utils/auth.ts, packages/trpc/src/routers/project.ts, packages/validation/src/index.ts
Enforce revenue tracking only when project allows or client secret present; project create/update flow and validation schema include allowUnsafeRevenueTracking.
DB migrations & schema
packages/db/code-migrations/6-add-revenue-column.ts, packages/db/prisma/migrations/..._add_revenue_tracking_setting_on_project/migration.sql, packages/db/prisma/schema.prisma
Migration to add revenue column; Prisma migration to add allowUnsafeRevenueTracking on Project; schema updated.
Session buffer & consistency
packages/db/src/buffers/session-buffer.ts, packages/db/src/session-consistency.ts
getExistingSession refactored to accept options (sessionId or projectId+profileId); session revenue init/merge and dual Redis key writes added; session-consistency read path replaced by retry/backoff (excludes Session model).
Event services & worker
packages/db/src/services/event.service.ts, apps/worker/src/jobs/events.incoming-event.ts, apps/worker/src/jobs/events.create-session-end.ts, apps/worker/src/jobs/events.incoming-events.test.ts
Introduced transformSessionToEvent, added optional revenue on event types, worker switched to sessionBuffer lookups, payloads now include revenue and use session fields; tests adjusted to mock sessionBuffer.
Overview / metrics / charts
packages/db/src/services/overview.service.ts, packages/db/src/services/chart.service.ts, packages/trpc/src/routers/chart.ts, packages/trpc/src/routers/overview.ts
Injected total_revenue across metrics/series/top queries; chart router threads timezone and exposes revenue; overview adds prev_total_revenue; chart aggregations handle revenue-specific filters.
Frontend charts & UI
apps/start/src/components/projects/project-chart.tsx, apps/start/src/components/overview/overview-metrics.tsx, apps/start/src/components/overview/overview-metric-card.tsx, apps/start/src/components/charts/common-bar.tsx
New ProjectChart component with revenue bars and right Y axis; overview chart UI and metric cards handle currency unit and revenue rendering; added green BarShape.
Tables, widgets & responsive
apps/start/src/components/overview/overview-widget-table.tsx, apps/start/src/components/widget-table.tsx
Added revenue column and RevenuePieChart; introduced ColumnResponsive, container-query based responsive system and helpers.
Formatting & utils
apps/start/src/hooks/use-numer-formatter.ts, apps/start/src/hooks/use-format-date-interval.ts, apps/start/src/components/report-chart/common/*
formatCurrency accepts options with short; date-interval formatting refactored to { interval, short }; call sites updated.
Settings, titles & routes
apps/start/src/components/settings/edit-project-details.tsx, apps/start/src/utils/title.ts, apps/start/src/routes/...profile..._tabs.*
Added allowUnsafeRevenueTracking UI in project settings; added PROFILE_EVENTS / PROFILE_DETAILS titles; route head() title hooks added.
Profile & events UI
apps/start/src/components/profiles/profile-metrics.tsx, apps/start/src/components/events/event-icon.tsx, apps/start/src/components/events/table/item.tsx
Added revenue metric and hideOnZero behavior; event icon map includes revenue; minor spacing/typography tweaks in event item.
Docs & public UI
apps/public/components/flow-step.tsx, apps/public/content/docs/get-started/revenue-tracking.mdx, apps/public/package.json
New FlowStep component; comprehensive revenue-tracking MDX doc added; dev script port set to 3001.
Migrations / ClickHouse tooling
packages/db/src/clickhouse/migration.ts, packages/db/code-migrations/3-init-ch.sql (removed), packages/db/code-migrations/5-add-imports-table.sql (removed), .gitignore
Migration helper emits replicated+local ALTERs for clusters; two large SQL migration files removed; gitignore updated to exclude generated SQL migration files.
Smaller UI & tooling tweaks
assorted files
Tooltip props extended, ChartSSR removed, real-time histogram and reloader refactored, number formatting hook usages updated, and other small UX/formatting changes across start app.

Sequence Diagram(s)

sequenceDiagram
    participant SDK as Client SDK
    participant Web as Web SDK
    participant API as Track API
    participant Redis as Redis (sessionBuffer)
    participant Worker as Event Worker
    participant DB as ClickHouse

    Note over SDK,Web: Emitting revenue from client
    SDK->>API: POST /track { name: 'revenue', __revenue, __deviceId?, ... }
    Web->>Web: pendingRevenue() enqueues or flushRevenue() sends queued revenue
    API->>API: validate revenue allowed (allowUnsafeRevenueTracking or client secret)
    API->>Redis: fetch device/session keys (multi-exists / getExistingSession)
    Redis-->>API: deviceId/session data
    API->>Worker: enqueue revenue event
    Worker->>Redis: sessionBuffer.getExistingSession({ sessionId } or by profile)
    Redis-->>Worker: session record (includes revenue)
    Worker->>Worker: aggregate/merge revenue into session
    Worker->>DB: insert event record (includes revenue field)
    DB-->>Worker: acknowledgement
Loading
sequenceDiagram
    participant Frontend as Overview UI
    participant tRPC as TRPC Router
    participant Service as OverviewService
    participant CH as ClickHouse

    Frontend->>tRPC: getMetrics({ projectId, range })
    tRPC->>Service: getMetrics(...)
    Service->>CH: query metrics including total_revenue and series
    CH-->>Service: metrics + total_revenue
    Service-->>tRPC: enriched payload with total_revenue, prev_total_revenue
    tRPC-->>Frontend: response
    Frontend->>Frontend: render composed chart (value line + revenue bars) and revenue blocks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • packages/db/src/buffers/session-buffer.ts — dual-lookup keys, expiry, revenue init/merge correctness.
  • apps/worker/src/jobs/* — transformSessionToEvent usage, revenue propagation, timing of session-end event creation.
  • packages/db/src/services/overview.service.ts and chart.service.ts — SQL/aggregation correctness for total_revenue and clustering edge cases.
  • packages/db/src/session-consistency.ts and packages/db/src/clickhouse/migration.ts — cluster-aware ALTERs and retry/backoff logic behavior.
  • API validation (apps/api/src/utils/auth.ts) — ensure allowUnsafeRevenueTracking + client-secret logic blocks/permits correctly.

Possibly related PRs

Poem

🐰
I hopped through buffers, nibbling bytes of green,
Counting tiny payments in a meadow keen.
Charts now sparkle with a golden line,
Device-IDs fetched, the numbers align.
A carrot for every revenue scene!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feature/revenue 2' is vague and generic, using a non-descriptive term that does not convey meaningful information about the changeset despite substantial revenue tracking feature additions. Revise the title to be more specific and descriptive, such as 'Add revenue tracking feature with SDK methods and dashboard visualization' to clearly communicate the primary purpose of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 feature/revenue-2

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85f87e2 and adeec9c.

📒 Files selected for processing (7)
  • apps/api/src/controllers/event.controller.ts (2 hunks)
  • apps/api/src/controllers/profile.controller.ts (1 hunks)
  • apps/api/src/controllers/track.controller.ts (5 hunks)
  • apps/api/src/utils/auth.ts (1 hunks)
  • apps/start/src/components/overview/overview-metric-card.tsx (6 hunks)
  • apps/start/src/components/report-chart/common/axis.tsx (1 hunks)
  • packages/db/src/clickhouse/migration.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/src/controllers/track.controller.ts
  • apps/api/src/utils/auth.ts
  • apps/start/src/components/report-chart/common/axis.tsx
🧰 Additional context used
📓 Path-based instructions (1)
apps/start/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/start/.cursorrules)

apps/start/**/*.{ts,tsx}: Instrument server functions created with createServerFn by wrapping their implementation in Sentry.startSpan and importing Sentry from '@sentry/tanstackstart-react'
When using Sentry in the TanStack Start React app, import it as: import * as Sentry from '@sentry/tanstackstart-react'

Files:

  • apps/start/src/components/overview/overview-metric-card.tsx
🧬 Code graph analysis (1)
apps/start/src/components/overview/overview-metric-card.tsx (3)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/date.ts (1)
  • formatDate (19-27)
apps/public/components/ui/tooltip.tsx (1)
  • Tooltiper (36-51)
⏰ 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). (3)
  • GitHub Check: build-and-push-worker
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-api
🔇 Additional comments (4)
packages/db/src/clickhouse/migration.ts (2)

143-146: Excellent fix for clustered column additions.

The dual ALTER approach now correctly updates both the replicated (data-bearing) table and the distributed table's metadata. This addresses the inconsistency flagged in the previous review.


163-166: Excellent fix for clustered column drops.

The dual ALTER approach now correctly updates both the replicated table and the distributed table's metadata, matching the pattern established in addColumns. This addresses the inconsistency flagged in the previous review.

apps/start/src/components/overview/overview-metric-card.tsx (1)

88-91: Currency conversion looks correct

The logic properly converts revenue from cents (storage format) to dollars for display, and uses the appropriate currency formatter.

apps/api/src/controllers/event.controller.ts (1)

50-55: The original review is incorrect and should be disregarded.

The review assumes that when ua is undefined, parseUserAgent returns isServer: false, causing groupId to become an empty string. However, the code shows that when ua is undefined, parseUserAgent returns parsedServerUa with isServer: true.

When isServer is true, groupId is derived from request.body?.profileId or a generated ID (lines 52-54), not from currentDeviceId. Therefore, groupId will never be empty—it will always be either ${projectId}:${request.body?.profileId} or ${projectId}:${generateId()}.

The code correctly handles the case where ua is missing.

Likely an incorrect or invalid review comment.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/start/src/components/overview/overview-metrics.tsx (1)

104-112: Remove leftover debug logging.

This console.log('data', data) runs on every render and dumps all metric values (including revenue) to the browser console. It’s noisy in production and leaks sensitive telemetry. Please drop it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 447b766 and 79c3a66.

📒 Files selected for processing (25)
  • apps/api/src/controllers/profile.controller.ts (1 hunks)
  • apps/api/src/routes/profile.router.ts (1 hunks)
  • apps/public/components/flow-step.tsx (1 hunks)
  • apps/public/content/docs/get-started/revenue-tracking.mdx (1 hunks)
  • apps/start/src/components/charts/common-bar.tsx (1 hunks)
  • apps/start/src/components/events/event-icon.tsx (1 hunks)
  • apps/start/src/components/overview/overview-metric-card.tsx (2 hunks)
  • apps/start/src/components/overview/overview-metrics.tsx (12 hunks)
  • apps/worker/src/jobs/events.create-session-end.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-event.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-events.test.ts (1 hunks)
  • packages/db/code-migrations/6-add-revenue-column.sql (1 hunks)
  • packages/db/code-migrations/6-add-revenue-column.ts (1 hunks)
  • packages/db/src/buffers/event-buffer.test.ts (0 hunks)
  • packages/db/src/buffers/event-buffer.ts (1 hunks)
  • packages/db/src/buffers/session-buffer.ts (5 hunks)
  • packages/db/src/clickhouse/client.ts (1 hunks)
  • packages/db/src/services/chart.service.ts (1 hunks)
  • packages/db/src/services/event.service.ts (5 hunks)
  • packages/db/src/services/overview.service.ts (12 hunks)
  • packages/sdks/sdk/src/index.ts (1 hunks)
  • packages/sdks/web/src/index.ts (3 hunks)
  • packages/sdks/web/src/types.d.ts (1 hunks)
  • packages/trpc/src/routers/chart.ts (1 hunks)
  • packages/trpc/src/routers/overview.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • packages/db/src/buffers/event-buffer.test.ts
🧰 Additional context used
📓 Path-based instructions (1)
apps/start/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/start/.cursorrules)

apps/start/**/*.{ts,tsx}: Instrument server functions created with createServerFn by wrapping their implementation in Sentry.startSpan and importing Sentry from '@sentry/tanstackstart-react'
When using Sentry in the TanStack Start React app, import it as: import * as Sentry from '@sentry/tanstackstart-react'

Files:

  • apps/start/src/components/events/event-icon.tsx
  • apps/start/src/components/charts/common-bar.tsx
  • apps/start/src/components/overview/overview-metric-card.tsx
  • apps/start/src/components/overview/overview-metrics.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-26T18:30:17.476Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: .cursorrules:0-0
Timestamp: 2025-09-26T18:30:17.476Z
Learning: When writing ClickHouse queries, use the helper functions in ./packages/db/src/clickhouse/query-functions.ts rather than crafting raw queries

Applied to files:

  • packages/db/code-migrations/6-add-revenue-column.ts
🧬 Code graph analysis (8)
packages/db/src/services/overview.service.ts (1)
packages/db/src/clickhouse/query-builder.ts (1)
  • clix (650-652)
packages/db/src/services/event.service.ts (3)
packages/db/src/services/profile.service.ts (1)
  • IServiceProfile (216-240)
packages/db/src/services/session.service.ts (1)
  • IClickhouseSession (17-57)
packages/db/src/clickhouse/client.ts (1)
  • convertClickhouseDateToJs (293-295)
packages/sdks/web/src/index.ts (1)
packages/sdks/sdk/src/index.ts (1)
  • OpenPanel (73-212)
packages/db/code-migrations/6-add-revenue-column.ts (2)
packages/db/code-migrations/helpers.ts (1)
  • getIsCluster (17-24)
packages/db/src/clickhouse/migration.ts (2)
  • addColumns (137-152)
  • runClickhouseMigrationCommands (338-470)
apps/api/src/controllers/profile.controller.ts (3)
packages/db/src/services/salt.service.ts (1)
  • getSalts (20-49)
apps/api/src/utils/get-client-ip.ts (1)
  • getClientIp (6-8)
packages/redis/redis.ts (1)
  • getRedisCache (66-72)
apps/worker/src/jobs/events.incoming-event.ts (1)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
apps/start/src/components/overview/overview-metrics.tsx (3)
packages/common/src/get-previous-metric.ts (1)
  • getPreviousMetric (7-39)
apps/public/lib/utils.ts (1)
  • cn (4-6)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
apps/worker/src/jobs/events.create-session-end.ts (3)
packages/db/src/services/event.service.ts (3)
  • IServiceEvent (199-237)
  • getEvents (313-356)
  • transformSessionToEvent (104-151)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
packages/common/src/date.ts (1)
  • getTime (5-7)
🪛 LanguageTool
apps/public/content/docs/get-started/revenue-tracking.mdx

[grammar] ~174-~174: Ensure spelling is correct
Context: ... work if your OpenPanel instance didn't recieve a client secret (for securuty reasons)....

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~174-~174: Ensure spelling is correct
Context: ...nce didn't recieve a client secret (for securuty reasons). You can enable frontend reven...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (28)
packages/db/src/clickhouse/client.ts (1)

29-31: Verify that silencing debug logs is intentional.

The logger.info(message, args) call in the debug method is now commented out, which means ClickHouse client debug messages will no longer appear in logs. This may complicate troubleshooting in development or when diagnosing production issues.

Can you confirm this is intentional to reduce log noise in production? If so, consider whether a conditional check (e.g., based on NODE_ENV) would allow debug logs in development while keeping production clean.

apps/start/src/components/events/event-icon.tsx (1)

51-54: LGTM!

The revenue icon entry is properly configured with DollarSignIcon (mapped at line 135) and green color, following the same pattern as other event icons.

apps/start/src/components/charts/common-bar.tsx (1)

80-88: LGTM!

The BarShapeGreen definition follows the same pattern as BarShapeBlue and BarShapeGrey, with appropriate green color values and consistent opacity transitions for active states.

apps/public/components/flow-step.tsx (1)

1-79: LGTM!

The FlowStep component is well-structured with:

  • Clear prop types and sensible defaults
  • Proper icon and color mapping
  • Conditional connector line rendering
  • Good separation of concerns (layout, styling, content)

The component will serve well for visualizing flow sequences in the documentation.

apps/public/content/docs/get-started/revenue-tracking.mdx (1)

1-202: Excellent documentation structure.

The revenue tracking documentation is comprehensive and well-organized:

  • Clear explanation of the device_id linking mechanism
  • Two distinct approaches (webhook vs frontend) with practical code examples
  • Good use of FlowStep components for visual clarity
  • Helpful pros/cons comparison
  • Complete API reference for available methods

The Stripe integration examples will be especially helpful for users implementing revenue tracking.

apps/api/src/routes/profile.router.ts (1)

28-32: LGTM!

The new GET route for profile info is properly configured and follows the same pattern as existing routes. Having both GET and POST on the root path is valid since they use different HTTP methods.

packages/db/code-migrations/6-add-revenue-column.sql (1)

1-1: LGTM!

The migration is well-designed:

  • IF NOT EXISTS ensures idempotency
  • UInt64 is appropriate for revenue values (can handle large amounts in smallest currency units)
  • CODEC(Delta(4), LZ4) provides efficient storage for numeric data
packages/sdks/sdk/src/index.ts (1)

186-191: Verify backend handling of __revenue property.

The revenue method tracks an event named 'revenue' with the amount passed as the __revenue property. This implementation is clean and delegates to the existing track method.

Please confirm that:

  1. The backend expects the revenue amount in the __revenue property
  2. This aligns with the database migration that adds a revenue column (packages/db/code-migrations/6-add-revenue-column.sql)
  3. The event service properly extracts __revenue from properties and maps it to the revenue column

You can verify this by checking the event transformation logic in the backend services.

packages/trpc/src/routers/chart.ts (1)

209-210: LGTM! Revenue property added to queryable properties.

The addition of 'revenue' to the properties list allows frontend queries to filter and analyze revenue data alongside other event properties.

apps/start/src/components/overview/overview-metric-card.tsx (2)

25-25: LGTM! Currency unit type added.

The new 'currency' unit option enables revenue metrics to be formatted as currency values.


60-63: LGTM! Currency formatting with cents-to-dollars conversion.

The implementation correctly converts revenue from cents to dollars before formatting. This assumes revenue is consistently stored in cents throughout the system.

packages/trpc/src/routers/overview.ts (2)

237-237: LGTM! Previous revenue metric added to stats endpoint.

The addition follows the established pattern for previous-period metrics, maintaining consistency with other comparative fields like prev_bounce_rate and prev_unique_visitors.


250-250: LGTM! Previous revenue added to series data.

Consistent with the metrics-level change, this enables time-series comparison of revenue data.

packages/db/src/services/chart.service.ts (4)

176-182: LGTM! Revenue-specific handling for property_sum.

The specialized path for revenue correctly uses the native column with revenue > 0 filtering, which is more appropriate than the generic property extraction logic.


186-192: LGTM! Revenue-specific handling for property_average.

Consistent with the property_sum implementation, this correctly aggregates revenue values from the native column.


196-202: LGTM! Revenue-specific handling for property_max.

The pattern is correctly applied for maximum revenue calculations.


206-212: LGTM! Revenue-specific handling for property_min.

The pattern is correctly applied for minimum revenue calculations, completing the set of aggregation operations.

packages/db/code-migrations/6-add-revenue-column.ts (3)

9-14: LGTM! Revenue column migration with appropriate data type and compression.

The UInt64 type is suitable for storing revenue in cents, and the CODEC(Delta(4), LZ4) provides efficient compression for monetary values. Delta encoding is particularly effective for incremental numerical data.


16-27: LGTM! SQL file generation for migration tracking.

Writing the migration SQL to a file provides useful documentation and debugging capabilities.


29-31: LGTM! Dry-run support for safe migration testing.

The --dry flag allows validation of the migration SQL without executing it, which is a good safety practice.

apps/api/src/controllers/profile.controller.ts (4)

15-31: LGTM! Input validation for profile info endpoint.

The function properly validates required inputs (projectId, IP address, and user-agent) and returns appropriate 400 errors when missing.


32-44: LGTM! Device ID generation with salt rotation support.

Generating device IDs with both current and previous salts ensures continuity during salt rotation periods.


45-61: LGTM! Efficient Redis lookup with multi command.

Using Redis multi to batch both EXISTS checks is efficient and correctly returns the matching device ID.


62-68: Empty deviceId fallback is safe; manually verify any external consumers.

The info endpoint intentionally returns an empty string for deviceId when Redis operations fail or no session is found (lines 66-68). This is a safe fallback—returning an empty string is preferable to null or undefined and the inferred return type { deviceId: string } explicitly allows it.

No validation constraints or broken dependencies were found in the codebase. However, since external API consumers may exist outside the visible codebase, verify that any clients or SDKs calling this endpoint gracefully handle the empty deviceId response.

packages/sdks/web/src/types.d.ts (1)

11-15: LGTM! Revenue tracking methods added to SDK public API.

The additions expose four revenue-related methods (revenue, flushRevenue, clearRevenue, pendingRevenue) to the web SDK's public interface, enabling client-side revenue tracking functionality.

apps/worker/src/jobs/events.incoming-events.test.ts (1)

1-1: LGTM! Test imports updated to reflect session-based event processing.

The removal of IServiceEvent import aligns with the migration from lastScreenView-based logic to session-based event processing.

packages/db/src/services/overview.service.ts (1)

150-327: Revenue totals wired correctly
The rollup-based total_revenue plumbing mirrors the existing bounce/session patterns and cleanly surfaces both daily and aggregate values. Looks good.

apps/worker/src/jobs/events.create-session-end.ts (1)

45-109: Session-derived last event flow looks solid
Pulling the session snapshot via transformSessionToEvent keeps the notification stream ordered while ensuring the session_end payload reuses the authoritative session data. Nice cleanup.

Comment thread apps/public/content/docs/get-started/revenue-tracking.mdx Outdated
Comment thread apps/start/src/components/overview/overview-metrics.tsx Outdated
Comment thread packages/db/src/buffers/session-buffer.ts Outdated
Comment thread packages/sdks/web/src/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/trpc/src/routers/overview.ts (1)

110-119: Restore visitor counts for anonymous traffic.

Switching visitor_count to uniq(profile_id) means any event without an identified profile now contributes zero visitors. Most traffic arrives before identification, so the realtime chart will collapse to 0 for anonymous sessions. Please revert to counting a stable identifier that exists for every session (e.g., session_id or coalesce(profile_id, device_id, session_id)), so anonymous visitors remain visible.

packages/db/src/services/overview.service.ts (1)

208-227: Ensure filled series returns numeric revenue values

Rows produced by WITH FILL don’t have a matching daily_stats entry, so ds.total_revenue remains the default for a nullable column — i.e. NULL. That leaves the series emitting null even though the public type promises number, and downstream charting will see gaps instead of zeros. ClickHouse fills only the ordering column; every other column keeps its default value, and for Nullable(...) that default is NULL. (docs-content.clickhouse.tech)

Wrap the join column with coalesce (or ifNull) so gap rows report 0, and update the GROUP BY list to match the new expression:

-          'ds.total_revenue AS total_revenue',
+          'coalesce(ds.total_revenue, 0) AS total_revenue',

-        .groupBy(['date', 'ds.bounce_rate', 'ds.total_revenue'])
+        .groupBy([
+          'date',
+          'ds.bounce_rate',
+          'coalesce(ds.total_revenue, 0)',
+        ])

coalesce is built for exactly this nullable-to-default projection. (clickhouse.com)

♻️ Duplicate comments (4)
packages/sdks/web/src/index.ts (1)

43-47: Guard sessionStorage access to avoid crashing the SDK.

This issue was previously flagged: sessionStorage.getItem and JSON.parse can throw in Safari private mode, when storage quota is exhausted, or with corrupted/legacy values (including the literal string "null"). Currently, any exception here prevents the entire SDK from initializing, breaking all tracking.

Wrap the access in a try-catch block:

     if (!this.isServer()) {
       console.log('OpenPanel.dev - Initialized', this.options);
 
-      const pending = sessionStorage.getItem('openpanel-pending-revenues');
-      if (pending) {
-        this.pendingRevenues = JSON.parse(pending);
-      }
+      try {
+        const pending = sessionStorage.getItem('openpanel-pending-revenues');
+        if (pending && pending !== 'null') {
+          this.pendingRevenues = JSON.parse(pending);
+        }
+      } catch (error) {
+        console.warn('OpenPanel.dev - Failed to load pending revenues', error);
+        this.pendingRevenues = [];
+      }
 
       this.setGlobalProperties({
apps/public/content/docs/get-started/revenue-tracking.mdx (1)

330-330: Fix the remaining spelling mistakes.

This sentence still contains the previously flagged typos (“recieve”, “securuty”). Please update it to use “receive” and “security”.

-The revenue method will create a revenue event. It's important to know that this method will not work if your OpenPanel instance didn't recieve a client secret (for securuty reasons). You can enable frontend revenue tracking within your project settings.
+The revenue method will create a revenue event. It's important to know that this method will not work if your OpenPanel instance didn't receive a client secret (for security reasons). You can enable frontend revenue tracking within your project settings.
apps/start/src/components/overview/overview-metrics.tsx (1)

111-112: Remove debug logging before shipping. These console.log calls spam render-time data (including revenue) into production consoles again, contradicting the earlier review directive and leaking sensitive metrics. Drop both logs (console.log('data', data) and console.log('data', dotIndex)) before shipping.

packages/db/src/buffers/session-buffer.ts (1)

95-96: Guard the revenue accumulator against undefined. Legacy sessions often have revenue unset; using += in that state turns the field into NaN, corrupting sessions and downstream revenue sums. Initialize to 0 before adding.

-      newSession.revenue += event.name === 'revenue' ? (event.revenue ?? 0) : 0;
+      const revenueDelta =
+        event.name === 'revenue' ? (event.revenue ?? 0) : 0;
+      newSession.revenue = (newSession.revenue ?? 0) + revenueDelta;
🧹 Nitpick comments (2)
apps/start/src/components/events/table/item.tsx (1)

95-129: Consider displaying revenue data in the event metadata row.

Since this PR introduces revenue tracking and IServiceEvent now includes an optional revenue field, you might want to display revenue alongside other event metadata (referrer, OS, browser, etc.) when present.

For example, you could add a revenue Pill similar to the existing metadata pills:

{event.revenue != null && viewOptions.revenue !== false && (
  <Pill icon={<SerieIcon name="revenue" />}>
    {formatCurrency(event.revenue)}
  </Pill>
)}

This would make revenue immediately visible in event lists, enhancing the revenue tracking feature's visibility.

packages/db/src/services/chart.service.ts (1)

176-214: Consider extracting the revenue property logic to reduce duplication.

The revenue-specific handling is repeated across four aggregation types (sum, average, max, min). While functionally correct, this pattern could be refactored into a helper function to improve maintainability.

For example:

function getAggregationSelect(
  aggregation: 'sum' | 'avg' | 'max' | 'min',
  property: string
): { select: string; where: string } {
  if (property === 'revenue') {
    return {
      select: `${aggregation}(revenue) as count`,
      where: 'revenue > 0'
    };
  }
  return {
    select: `${aggregation}(toFloat64(${getSelectPropertyKey(property)})) as count`,
    where: `${getSelectPropertyKey(property)} IS NOT NULL AND notEmpty(${getSelectPropertyKey(property)})`
  };
}

Then use it in each segment block.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79c3a66 and 15e476c.

📒 Files selected for processing (34)
  • apps/api/src/controllers/profile.controller.ts (1 hunks)
  • apps/api/src/controllers/track.controller.ts (4 hunks)
  • apps/api/src/routes/track.router.ts (2 hunks)
  • apps/public/components/flow-step.tsx (1 hunks)
  • apps/public/content/docs/get-started/revenue-tracking.mdx (1 hunks)
  • apps/public/package.json (1 hunks)
  • apps/start/src/components/charts/common-bar.tsx (1 hunks)
  • apps/start/src/components/events/event-icon.tsx (1 hunks)
  • apps/start/src/components/events/table/item.tsx (4 hunks)
  • apps/start/src/components/overview/overview-live-histogram.tsx (1 hunks)
  • apps/start/src/components/overview/overview-metric-card.tsx (2 hunks)
  • apps/start/src/components/overview/overview-metrics.tsx (12 hunks)
  • apps/start/src/components/overview/overview-widget-table.tsx (8 hunks)
  • apps/start/src/components/realtime/realtime-live-histogram.tsx (1 hunks)
  • apps/start/src/components/realtime/realtime-reloader.tsx (1 hunks)
  • apps/start/src/components/widget-table.tsx (4 hunks)
  • apps/start/src/hooks/use-numer-formatter.ts (1 hunks)
  • apps/worker/src/jobs/events.create-session-end.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-event.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-events.test.ts (1 hunks)
  • packages/db/code-migrations/6-add-revenue-column.sql (1 hunks)
  • packages/db/code-migrations/6-add-revenue-column.ts (1 hunks)
  • packages/db/src/buffers/event-buffer.test.ts (0 hunks)
  • packages/db/src/buffers/event-buffer.ts (1 hunks)
  • packages/db/src/buffers/session-buffer.ts (5 hunks)
  • packages/db/src/clickhouse/client.ts (1 hunks)
  • packages/db/src/services/chart.service.ts (1 hunks)
  • packages/db/src/services/event.service.ts (5 hunks)
  • packages/db/src/services/overview.service.ts (16 hunks)
  • packages/sdks/sdk/src/index.ts (1 hunks)
  • packages/sdks/web/src/index.ts (3 hunks)
  • packages/sdks/web/src/types.d.ts (1 hunks)
  • packages/trpc/src/routers/chart.ts (1 hunks)
  • packages/trpc/src/routers/overview.ts (4 hunks)
💤 Files with no reviewable changes (1)
  • packages/db/src/buffers/event-buffer.test.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/start/src/components/overview/overview-live-histogram.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/start/src/components/overview/overview-metric-card.tsx
  • packages/db/src/clickhouse/client.ts
  • packages/sdks/sdk/src/index.ts
  • apps/start/src/components/events/event-icon.tsx
  • apps/worker/src/jobs/events.incoming-events.test.ts
  • apps/worker/src/jobs/events.incoming-event.ts
  • packages/sdks/web/src/types.d.ts
  • packages/db/src/services/event.service.ts
  • apps/public/components/flow-step.tsx
  • packages/db/code-migrations/6-add-revenue-column.ts
  • packages/trpc/src/routers/chart.ts
🧰 Additional context used
📓 Path-based instructions (1)
apps/start/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/start/.cursorrules)

apps/start/**/*.{ts,tsx}: Instrument server functions created with createServerFn by wrapping their implementation in Sentry.startSpan and importing Sentry from '@sentry/tanstackstart-react'
When using Sentry in the TanStack Start React app, import it as: import * as Sentry from '@sentry/tanstackstart-react'

Files:

  • apps/start/src/components/events/table/item.tsx
  • apps/start/src/components/realtime/realtime-live-histogram.tsx
  • apps/start/src/components/realtime/realtime-reloader.tsx
  • apps/start/src/hooks/use-numer-formatter.ts
  • apps/start/src/components/overview/overview-metrics.tsx
  • apps/start/src/components/charts/common-bar.tsx
  • apps/start/src/components/widget-table.tsx
  • apps/start/src/components/overview/overview-widget-table.tsx
🧬 Code graph analysis (9)
apps/api/src/controllers/track.controller.ts (4)
packages/sdks/sdk/src/index.ts (1)
  • fetchDeviceId (199-206)
packages/db/src/services/salt.service.ts (1)
  • getSalts (20-49)
apps/api/src/utils/get-client-ip.ts (1)
  • getClientIp (6-8)
packages/redis/redis.ts (1)
  • getRedisCache (66-72)
apps/start/src/components/realtime/realtime-live-histogram.tsx (3)
apps/start/src/components/charts/common-bar.tsx (1)
  • BarShapeBlue (71-79)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
apps/worker/src/jobs/events.create-session-end.ts (3)
packages/db/src/services/event.service.ts (3)
  • IServiceEvent (199-237)
  • getEvents (313-356)
  • transformSessionToEvent (104-151)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
packages/common/src/date.ts (1)
  • getTime (5-7)
apps/start/src/components/overview/overview-metrics.tsx (4)
packages/common/src/get-previous-metric.ts (1)
  • getPreviousMetric (7-39)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/public/lib/utils.ts (1)
  • cn (4-6)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
apps/api/src/routes/track.router.ts (2)
apps/api/src/controllers/track.controller.ts (1)
  • fetchDeviceId (410-467)
packages/sdks/sdk/src/index.ts (1)
  • fetchDeviceId (199-206)
apps/start/src/components/widget-table.tsx (1)
apps/public/lib/utils.ts (1)
  • cn (4-6)
packages/db/src/services/overview.service.ts (1)
packages/db/src/clickhouse/query-builder.ts (1)
  • clix (650-652)
apps/start/src/components/overview/overview-widget-table.tsx (1)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
packages/sdks/web/src/index.ts (1)
packages/sdks/sdk/src/index.ts (1)
  • OpenPanel (73-227)
🪛 LanguageTool
apps/public/content/docs/get-started/revenue-tracking.mdx

[grammar] ~330-~330: Ensure spelling is correct
Context: ... work if your OpenPanel instance didn't recieve a client secret (for securuty reasons)....

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~330-~330: Ensure spelling is correct
Context: ...nce didn't recieve a client secret (for securuty reasons). You can enable frontend reven...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ 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). (3)
  • GitHub Check: build-and-push-api
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-worker
🔇 Additional comments (11)
apps/start/src/components/events/table/item.tsx (1)

57-57: LGTM! UI spacing refinements are well-executed.

The spacing adjustments tighten the layout consistently:

  • Reduced gaps improve visual density
  • Typography adjustments maintain readability
  • Padding tweaks work well for responsive design

Also applies to: 66-66, 80-80, 95-95

apps/start/src/components/widget-table.tsx (1)

93-170: Fix minWidth columns stuck hidden

getMinWidthClass() unconditionally returns the Tailwind hidden class, which sets display: none. Your container-query CSS only toggles padding/visibility, so the display: none never gets reverted. Any column using responsive.minWidth will therefore stay invisible at every breakpoint. Remove the hidden class (or explicitly restore display) so the container query rules can reveal the column once the container is wide enough.

 function getMinWidthClass(minWidth: number): string {
-  return 'hidden';
+  return '';
 }
apps/public/package.json (1)

6-6: LGTM!

Explicitly specifying port 3001 for the dev server improves predictability and prevents port conflicts.

apps/api/src/controllers/profile.controller.ts (1)

5-5: LGTM!

Good cleanup removing the unused import.

packages/sdks/web/src/index.ts (2)

23-26: LGTM!

The PendingRevenue type is well-defined and appropriately scoped for batching revenue events.


206-229: LGTM! The revenue batching design is well-structured.

The three methods (flushRevenue, clearRevenue, pendingRevenue) provide a clean API for managing revenue events across sessions. The use of sessionStorage for persistence is appropriate for this use case.

However, ensure the sessionStorage operations in clearRevenue and pendingRevenue also have appropriate error handling, similar to the constructor.

apps/api/src/controllers/track.controller.ts (2)

110-113: LGTM!

Extracting the override device ID from properties.__deviceId allows clients to specify their own device ID when needed, which is useful for cross-session tracking.


124-132: LGTM!

The fallback logic correctly prefers the override device ID when provided, maintaining backward compatibility with the UA-based generation.

apps/api/src/routes/track.router.ts (2)

1-1: LGTM!

The import is correctly added for the new endpoint.


33-47: LGTM!

The new GET /device-id route is well-structured with:

  • Appropriate HTTP method (GET for retrieval)
  • Proper schema definition for the response
  • Consistent use of existing hooks (clientHook, isBotHook)
packages/db/src/buffers/event-buffer.ts (1)

360-360: LGTM!

The reduction from 6 to 5 KEYS aligns with the removal of last-event persistence logic (KEYS[6] for lastEventKey). This is consistent with the architectural change to source last-event data from session-level data instead.

Comment thread apps/api/src/controllers/track.controller.ts
Comment thread apps/worker/src/jobs/events.create-session-end.ts
@@ -0,0 +1 @@
ALTER TABLE events ADD COLUMN IF NOT EXISTS `revenue` UInt64 CODEC(Delta(4), LZ4); No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that integer-only revenue representation is intentional.

The revenue column uses UInt64, which cannot represent decimal values. This means revenue must be stored as an integer (e.g., cents or smallest currency unit). Ensure that:

  1. The SDK and client code convert revenue to the appropriate integer representation (e.g., dollars × 100 for cents)
  2. The display/query logic converts back to the decimal representation
  3. Documentation clearly indicates the expected unit

Run the following script to check how revenue values are being set in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for revenue property assignments to verify integer handling
rg -n -C3 '__revenue|revenue.*=' --type=ts --type=js

Length of output: 6444


I can see from the output that revenue is being handled with a /100 conversion at display time (line 273 in overview-metrics.tsx), which suggests integer cents representation. Let me verify the type definitions and validation to ensure this is intentional and enforced.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check type definitions for revenue in TypeScript interfaces
rg -n 'revenue\s*:' --type=ts -C2 | head -60

echo "---"
# Look for revenue validation or parsing logic
rg -n 'revenue.*validat|revenue.*parse|revenue.*Decimal' --type=ts --type=js -C2 | head -40

Length of output: 3869


I need to check the SDK function signature and documentation to see if revenue format is documented or enforced there.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find the revenue() method signature in SDK
ast-grep --pattern 'revenue($_) {
  $$$
}'

echo "---"
# Look for any documentation comments around revenue
rg -n -B5 'revenue.*amount|amount.*revenue' --type=ts --type=js | grep -A5 -B5 'revenue' | head -50

echo "---"
# Check for README or documentation
fd -e md | head -10 | xargs -I {} grep -l 'revenue' {} 2>/dev/null || echo "No markdown files with revenue found"

Length of output: 839


Let me get the complete method signature with any JSDoc comments:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Get complete revenue() method including JSDoc
rg -n -B10 'revenue\s*\(' packages/sdks/sdk/src/index.ts | head -30

echo "---"
# Check if there are TypeScript branded types or Int types
rg -n 'type.*Revenue|interface.*Revenue' --type=ts -C3

echo "---"
# Check the full incoming event transformation
rg -n -B2 -A2 'body.name === .revenue.' apps/worker/src/jobs/events.incoming-event.ts

Length of output: 707


Add validation and documentation for revenue cents representation.

The integer-only UInt64 representation is confirmed intentional (evidenced by the display-side division by 100 and storage schema). However, verification found three gaps:

  1. Missing input validation: SDK method revenue(amount: number) and worker conversion accept floats with no validation. Passing 19.99 silently truncates to 19 instead of 1999 cents.
  2. No JSDoc documentation: The SDK revenue method lacks documentation explaining that amount must be an integer representing cents.
  3. No type safety: TypeScript's number type permits decimals. Consider stricter typing or runtime validation.

Add JSDoc to packages/sdks/sdk/src/index.ts (revenue method) documenting the cents requirement, and implement validation in both SDK and apps/worker/src/jobs/events.incoming-event.ts to enforce integer values before storage.

Comment thread packages/db/src/buffers/session-buffer.ts Outdated
@lindesvard lindesvard force-pushed the main branch 3 times, most recently from b18e60a to 8fbe944 Compare November 14, 2025 12:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/db/src/services/overview.service.ts (1)

186-227: Add null-safety to revenue field from LEFT JOIN.

On line 208, ds.total_revenue comes from a LEFT JOIN with daily_stats. For dates created by the fill() clause that don't have matching session data, this will be NULL in the result set. Consider using coalesce(ds.total_revenue, 0) to ensure consistent numeric values.

Note: This same pattern exists for ds.bounce_rate on line 201 (existing code), so this is consistent with current behavior. However, both fields would benefit from null-safety.

Apply this diff to add null-safety:

-          'ds.total_revenue AS total_revenue',
+          'coalesce(ds.total_revenue, 0) AS total_revenue',

For consistency, consider also updating bounce_rate:

-          'ds.bounce_rate as bounce_rate',
+          'coalesce(ds.bounce_rate, 0) as bounce_rate',
♻️ Duplicate comments (6)
apps/public/content/docs/get-started/revenue-tracking.mdx (1)

330-330: Fix spelling in revenue method description.

Line 330 still has the original typos:

  • recievereceive
  • securutysecurity

Apply:

-The revenue method will create a revenue event. It's important to know that this method will not work if your OpenPanel instance didn't recieve a client secret (for securuty reasons). You can enable frontend revenue tracking within your project settings.
+The revenue method will create a revenue event. It's important to know that this method will not work if your OpenPanel instance didn't receive a client secret (for security reasons). You can enable frontend revenue tracking within your project settings.
packages/sdks/web/src/index.ts (1)

40-47: Guard sessionStorage read and JSON.parse to avoid crashing the SDK.

Accessing sessionStorage.getItem and JSON.parse unguarded can throw (private mode, storage disabled/quota, corrupted/legacy values), which would break initialization on the client.

Wrap this logic in a try/catch and ensure the value is a non-empty, non-"null" string before parsing; otherwise fall back to an empty array:

-      const pending = sessionStorage.getItem('openpanel-pending-revenues');
-      if (pending) {
-        this.pendingRevenues = JSON.parse(pending);
-      }
+      try {
+        if (typeof window !== 'undefined' && window.sessionStorage) {
+          const pending = window.sessionStorage.getItem(
+            'openpanel-pending-revenues',
+          );
+          if (pending && pending !== 'null') {
+            const parsed = JSON.parse(pending);
+            this.pendingRevenues = Array.isArray(parsed) ? parsed : [];
+          }
+        }
+      } catch {
+        this.pendingRevenues = [];
+      }

Do something similar for writes below so storage errors don’t throw.

apps/start/src/components/overview/overview-metrics.tsx (2)

111-112: Remove leftover debug logging.

console.log('data', data); will spam the console on every render and may leak analytics data. It should be removed before shipping.

-  console.log('data', data);

293-303: Remove debug logging for dotIndex.

The console.log here will fire on every hover/move, adding noise and potentially impacting performance:

-  console.log('data', dotIndex);

Safe to delete.

packages/db/code-migrations/6-add-revenue-column.sql (1)

1-1: Idempotent revenue column migration looks good

ADD COLUMN IF NOT EXISTS makes this safe on repeated runs, and the codecs are consistent with typical event columns. The earlier review comment about using UInt64 for revenue (cents representation, validation, and documentation in SDK/worker code) still applies; nothing new is required in this SQL file itself.

apps/api/src/controllers/track.controller.ts (1)

380-440: Fix the incorrect error log message.

As flagged in the previous review, line 433 contains a copy-pasted error message referencing the wrong endpoint:

request.log.error('Error getting session end GET /profile', error);

This should reference /device-id instead of /profile.

Apply this fix:

-    request.log.error('Error getting session end GET /profile', error);
+    request.log.error('Error getting session end GET /device-id', error);
🧹 Nitpick comments (11)
apps/start/src/components/overview/overview-widget-table.tsx (2)

11-46: Consider clamping percentage and marking the SVG as decorative for a11y.

RevenuePieChart looks correct and lightweight. To make it more robust and accessible, consider:

  • Clamping percentage into [0, 1] before computing offset to avoid odd visuals if an out‑of‑range value ever slips through.
  • Adding aria-hidden="true" and/or role="presentation" on the <svg> if this is purely decorative, so it doesn’t add noise for screen readers.

151-158: Revenue typing/units and hasRevenue guard – verify semantics, especially for zero/negative values.

  • revenue is treated as a required number on pages and optional on generic rows, and both paths format values via number.currency(revenue / 100...). This assumes upstream data is always in the same minor‑unit (e.g. cents) scale and uses 0 (not null/undefined) when there is no revenue. Please confirm that the API / data layer enforces this to avoid subtle misformatting or NaNs.
  • The guards hasRevenue = data.some((item) => item.revenue > 0) / (item.revenue ?? 0) > 0 plus totalRevenue > 0 ? revenue / totalRevenue : 0 mean:
    • The Revenue column will not render if all values are <= 0.
    • Pie percentages will be 0 when totalRevenue <= 0, even if absolute (e.g. negative refund) values exist.
      If you plan to show refunds or negative net revenue, you may want a different condition (e.g. !== 0) and a share calculation that handles negative totals more explicitly.

Also applies to: 164-165, 374-376, 403-429

packages/trpc/src/trpc.ts (1)

194-213: Consider disabling cache writes in non-production environments.

The cache is now only read in production (line 194), but cache writes still occur in all environments (lines 206-212). This means non-production environments compute fresh data but still write to Redis unnecessarily.

Consider wrapping the cache write operation with the same environment check:

   const result = await next();

   // @ts-expect-error
-  if (result.data) {
+  if (result.data && process.env.NODE_ENV === 'production') {
     getRedisCache().setJson(
       key,
       ttl,
       // @ts-expect-error
       result.data,
     );
   }
   return result;
apps/start/src/hooks/use-numer-formatter.ts (1)

34-71: LGTM with a minor consideration for the currency symbol fallback.

The formatCurrency function now supports both regular and compact notation formatting. The implementation is backwards-compatible since the options parameter is optional.

Consider making the fallback currency symbol more robust. Line 62 falls back to '$' when the symbol cannot be extracted, which may not be appropriate for non-USD currencies:

       const parts = currencyFormatter.formatToParts(0);
       const symbol =
-        parts.find((part) => part.type === 'currency')?.value || '$';
+        parts.find((part) => part.type === 'currency')?.value || currency;
       return `${formatted} ${symbol}`;

This way, if the symbol extraction fails, it will fall back to the currency code (e.g., "EUR") rather than always using "$".

apps/public/content/docs/get-started/revenue-tracking.mdx (2)

323-323: Minor Markdown formatting nit in “Cons” bullet.

The bold text here includes a trailing space before the closing **:

- **You can't dive deeper into where this revenue came from. **For instance, ...

Consider removing the extra space so only the sentence is bold:

- **You can't dive deeper into where this revenue came from.** For instance, ...

360-364: Align fetchDeviceId docs with actual behavior and fix grammar.

Two small issues here:

  • Heading should read “Fetch your current user’s device id”.
  • In the SDK snippet, fetchDeviceId currently returns Promise<string | null> in @openpanel/sdk, so the docs should either show Promise<string | null> or clearly mention that it can return null and how to handle that.

Example:

-### Fetch your current users device id
+### Fetch your current user's device id

-op.fetchDeviceId(): Promise<string>
+op.fetchDeviceId(): Promise<string | null>

And optionally add a note that null means the device ID could not be determined.

packages/sdks/web/src/index.ts (2)

23-32: Consider typing PendingRevenue.properties to match revenue properties.

Right now PendingRevenue.properties is Record<string, unknown>, while OpenPanelBase.revenue expects TrackProperties & { deviceId?: string }. Aligning these types would improve safety and editor support:

-type PendingRevenue = {
-  amount: number;
-  properties?: Record<string, unknown>;
-};
+type PendingRevenue = {
+  amount: number;
+  properties?: TrackProperties & { deviceId?: string };
+};

This keeps pending revenue calls type-compatible with the base SDK.


206-212: Handle partial failures in flushRevenue more defensively.

Using Promise.all means if any single revenue call rejects, flushRevenue rejects and you keep all pending revenues, potentially causing double-counts on retry.

Consider:

  • Logging or tracking which entries failed.
  • Clearing only successfully-sent entries, or
  • At least wrapping each call to avoid a single failure aborting the whole batch.

For example (simplified):

-    const promises = this.pendingRevenues.map((pending) =>
-      super.revenue(pending.amount, pending.properties),
-    );
-    await Promise.all(promises);
-    this.clearRevenue();
+    const results = await Promise.allSettled(
+      this.pendingRevenues.map((pending) =>
+        super.revenue(pending.amount, pending.properties),
+      ),
+    );
+    if (results.every((r) => r.status === 'fulfilled')) {
+      this.clearRevenue();
+    }
+    // Optionally: keep failed entries only, or log failures for observability.

Not strictly required, but it would make revenue tracking more robust to transient failures.

apps/start/src/components/overview/overview-metrics.tsx (1)

334-475: Revenue Y-axis domain and bar rendering are mostly fine; consider a small guard.

The right Y-axis domain is computed as:

domain={[
  0,
  data.reduce(
    (max, item) => Math.max(max, item.total_revenue ?? 0),
    0,
  ) * 2,
]}

If all total_revenue values are 0, the domain becomes [0, 0], which some Recharts versions treat as invalid. Consider falling back to 'auto' or a minimal positive max in that case:

const maxRevenue = data.reduce(
  (max, item) => Math.max(max, item.total_revenue ?? 0),
  0,
);

<YAxis
  {...revenueYAxisProps}
  yAxisId="right"
  orientation="right"
- domain={[0, maxRevenue * 2]}
+ domain={maxRevenue > 0 ? [0, maxRevenue * 2] : [0, 'auto']}
  width={25}
/>

The revenue Bar + Cell setup and the main Area (with glow filter and dashed last segment logic) otherwise look solid.

packages/db/src/session-consistency.ts (1)

212-233: Session consistency retries only help null-returning reads and comment is outdated

This block now:

  • Only has an effect for operations that return null on a miss (e.g., findUnique / findFirst); findMany / aggregate / count will never hit the retry path because their results are never null.
  • No longer forces reads to primary via __internalParams.transaction, even though the surrounding comment still says it does.

Functionally that may be fine if your main concern is read-after-write for unique lookups, but it would be good to:

  • Update the comment to describe the current “retry on null result” behavior instead of “force to primary”.
  • Optionally reuse the top-level retry constants instead of introducing local MAX_RETRIES/INITIAL_RETRY_DELAY_MS so tuning stays in one place.
apps/start/src/components/projects/project-chart.tsx (1)

136-142: Revenue Y-axis domain uses 2x multiplier.

Line 139 sets the revenue domain to [0, maxRevenue * 2], which caps bars at 50% height. This is likely intentional to prevent revenue bars from dominating the line chart, but consider whether a smaller multiplier (e.g., 1.5x) might improve visibility for smaller revenue values.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15e476c and 04d5b0e.

📒 Files selected for processing (42)
  • apps/api/src/controllers/track.controller.ts (4 hunks)
  • apps/api/src/routes/track.router.ts (2 hunks)
  • apps/api/src/utils/auth.ts (1 hunks)
  • apps/public/components/flow-step.tsx (1 hunks)
  • apps/public/content/docs/get-started/revenue-tracking.mdx (1 hunks)
  • apps/public/package.json (1 hunks)
  • apps/start/src/components/chart-ssr.tsx (0 hunks)
  • apps/start/src/components/charts/chart-tooltip.tsx (2 hunks)
  • apps/start/src/components/charts/common-bar.tsx (1 hunks)
  • apps/start/src/components/events/event-icon.tsx (1 hunks)
  • apps/start/src/components/events/table/item.tsx (4 hunks)
  • apps/start/src/components/overview/overview-live-histogram.tsx (1 hunks)
  • apps/start/src/components/overview/overview-metric-card.tsx (2 hunks)
  • apps/start/src/components/overview/overview-metrics.tsx (12 hunks)
  • apps/start/src/components/overview/overview-widget-table.tsx (8 hunks)
  • apps/start/src/components/projects/project-card.tsx (7 hunks)
  • apps/start/src/components/projects/project-chart.tsx (1 hunks)
  • apps/start/src/components/realtime/realtime-live-histogram.tsx (1 hunks)
  • apps/start/src/components/realtime/realtime-reloader.tsx (1 hunks)
  • apps/start/src/components/settings/edit-project-details.tsx (3 hunks)
  • apps/start/src/components/widget-table.tsx (4 hunks)
  • apps/start/src/hooks/use-numer-formatter.ts (1 hunks)
  • apps/worker/src/jobs/events.create-session-end.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-event.ts (5 hunks)
  • apps/worker/src/jobs/events.incoming-events.test.ts (7 hunks)
  • packages/db/code-migrations/6-add-revenue-column.sql (1 hunks)
  • packages/db/code-migrations/6-add-revenue-column.ts (1 hunks)
  • packages/db/prisma/migrations/20251118100123_add_revenue_tracking_setting_on_project/migration.sql (1 hunks)
  • packages/db/prisma/schema.prisma (1 hunks)
  • packages/db/src/buffers/session-buffer.ts (5 hunks)
  • packages/db/src/services/chart.service.ts (1 hunks)
  • packages/db/src/services/event.service.ts (5 hunks)
  • packages/db/src/services/overview.service.ts (16 hunks)
  • packages/db/src/session-consistency.ts (1 hunks)
  • packages/sdks/sdk/src/index.ts (1 hunks)
  • packages/sdks/web/src/index.ts (3 hunks)
  • packages/sdks/web/src/types.d.ts (1 hunks)
  • packages/trpc/src/routers/chart.ts (2 hunks)
  • packages/trpc/src/routers/overview.ts (4 hunks)
  • packages/trpc/src/routers/project.ts (2 hunks)
  • packages/trpc/src/trpc.ts (1 hunks)
  • packages/validation/src/index.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/start/src/components/chart-ssr.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/start/src/components/overview/overview-live-histogram.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • apps/start/src/components/overview/overview-metric-card.tsx
  • apps/api/src/routes/track.router.ts
  • apps/public/package.json
  • packages/db/src/buffers/session-buffer.ts
  • apps/worker/src/jobs/events.create-session-end.ts
  • packages/db/code-migrations/6-add-revenue-column.ts
  • packages/db/src/services/event.service.ts
  • apps/start/src/components/realtime/realtime-live-histogram.tsx
  • packages/sdks/sdk/src/index.ts
  • apps/public/components/flow-step.tsx
🧰 Additional context used
📓 Path-based instructions (1)
apps/start/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/start/.cursorrules)

apps/start/**/*.{ts,tsx}: Instrument server functions created with createServerFn by wrapping their implementation in Sentry.startSpan and importing Sentry from '@sentry/tanstackstart-react'
When using Sentry in the TanStack Start React app, import it as: import * as Sentry from '@sentry/tanstackstart-react'

Files:

  • apps/start/src/components/projects/project-chart.tsx
  • apps/start/src/components/widget-table.tsx
  • apps/start/src/components/charts/chart-tooltip.tsx
  • apps/start/src/components/events/table/item.tsx
  • apps/start/src/hooks/use-numer-formatter.ts
  • apps/start/src/components/overview/overview-widget-table.tsx
  • apps/start/src/components/events/event-icon.tsx
  • apps/start/src/components/overview/overview-metrics.tsx
  • apps/start/src/components/charts/common-bar.tsx
  • apps/start/src/components/realtime/realtime-reloader.tsx
  • apps/start/src/components/settings/edit-project-details.tsx
  • apps/start/src/components/projects/project-card.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-26T18:30:17.476Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: .cursorrules:0-0
Timestamp: 2025-09-26T18:30:17.476Z
Learning: When writing ClickHouse queries, use the helper functions in ./packages/db/src/clickhouse/query-functions.ts rather than crafting raw queries

Applied to files:

  • apps/worker/src/jobs/events.incoming-events.test.ts
📚 Learning: 2025-09-26T18:30:17.476Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: .cursorrules:0-0
Timestamp: 2025-09-26T18:30:17.476Z
Learning: When writing ClickHouse queries, always construct them using the custom query builder in ./packages/db/src/clickhouse/query-builder.ts

Applied to files:

  • apps/worker/src/jobs/events.incoming-events.test.ts
🧬 Code graph analysis (12)
apps/start/src/components/projects/project-chart.tsx (4)
apps/start/src/components/charts/chart-tooltip.tsx (3)
  • createChartTooltip (41-95)
  • ChartTooltipHeader (16-20)
  • ChartTooltipItem (22-39)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
apps/public/lib/utils.ts (1)
  • cn (4-6)
apps/worker/src/jobs/events.incoming-event.ts (2)
packages/sdks/sdk/src/index.ts (1)
  • revenue (186-197)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
apps/start/src/components/widget-table.tsx (1)
apps/public/lib/utils.ts (1)
  • cn (4-6)
apps/start/src/components/charts/chart-tooltip.tsx (1)
apps/public/lib/utils.ts (1)
  • cn (4-6)
packages/db/src/services/overview.service.ts (1)
packages/db/src/clickhouse/query-builder.ts (1)
  • clix (650-652)
apps/start/src/components/overview/overview-widget-table.tsx (1)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/api/src/controllers/track.controller.ts (3)
packages/sdks/sdk/src/index.ts (1)
  • fetchDeviceId (199-206)
packages/db/src/services/salt.service.ts (1)
  • getSalts (20-49)
packages/redis/redis.ts (1)
  • getRedisCache (66-72)
packages/trpc/src/routers/chart.ts (3)
packages/db/src/services/organization.service.ts (1)
  • getSettingsForProject (292-305)
packages/db/src/clickhouse/client.ts (3)
  • chQuery (257-262)
  • TABLE_NAMES (49-61)
  • ch (155-189)
packages/db/src/clickhouse/query-builder.ts (1)
  • clix (650-652)
apps/start/src/components/overview/overview-metrics.tsx (3)
packages/common/src/get-previous-metric.ts (1)
  • getPreviousMetric (7-39)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
apps/worker/src/jobs/events.incoming-events.test.ts (3)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
packages/db/src/clickhouse/client.ts (1)
  • formatClickhouseDate (264-275)
packages/db/src/services/session.service.ts (1)
  • IClickhouseSession (17-57)
packages/sdks/web/src/index.ts (1)
packages/sdks/sdk/src/index.ts (1)
  • OpenPanel (73-227)
apps/start/src/components/projects/project-card.tsx (2)
apps/start/src/components/projects/project-chart.tsx (1)
  • ProjectChart (87-215)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
🪛 LanguageTool
apps/public/content/docs/get-started/revenue-tracking.mdx

[grammar] ~330-~330: Ensure spelling is correct
Context: ... work if your OpenPanel instance didn't recieve a client secret (for securuty reasons)....

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~330-~330: Ensure spelling is correct
Context: ...nce didn't recieve a client secret (for securuty reasons). You can enable frontend reven...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ 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). (3)
  • GitHub Check: build-and-push-api
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-worker
🔇 Additional comments (39)
apps/start/src/components/charts/common-bar.tsx (1)

80-88: LGTM! Clean addition of green bar variant.

The implementation correctly follows the established pattern for color variants. The green color scheme is well-suited for revenue visualization as indicated in the PR objectives.

apps/start/src/components/overview/overview-widget-table.tsx (2)

79-88: Column base class composition and comment look consistent with the new responsive prop.

Using cn to apply alignment and typography per index while delegating visibility to the responsive prop keeps concerns nicely separated and matches the comment about removing old responsive logic.


172-273: Responsive priorities across pages and generic tables appear coherent.

The chosen priority values (Path/primary column = 1, Sessions = 2, Revenue = 3, BR/Duration with higher numbers) match the comments about “always visible” vs. “hidden when space is tight” and give a sensible ordering of importance. Assuming WidgetTable interprets lower numbers as higher priority, this aligns well with the intended behavior on constrained layouts.

Also applies to: 383-444

apps/start/src/components/widget-table.tsx (1)

2-19: ColumnResponsive API and prop wiring look sound

The ColumnPriority / ColumnResponsive types and the new responsive field on columns are well-shaped and documented, and the default behavior (“always visible” when responsive is omitted) is clear and consistent with the implementation.

Also applies to: 27-31

apps/start/src/components/events/event-icon.tsx (1)

51-54: LGTM!

The revenue icon mapping is properly configured with DollarSignIcon and green color, both of which are already available in the EventIconMapper and EventIconColors arrays.

apps/start/src/components/charts/chart-tooltip.tsx (1)

22-39: LGTM!

The optional styling props are well-designed, allowing consumers to customize both the outer container and inner content wrapper. The cn utility properly merges the default classes with custom ones.

apps/start/src/components/events/table/item.tsx (1)

57-95: LGTM!

The spacing adjustments create a more compact and consistent event list item layout. The removal of the "Event:" label in the non-screen_view branch simplifies the display while maintaining clarity.

packages/db/src/services/chart.service.ts (1)

176-214: LGTM!

The revenue-specific handling is consistently implemented across all property aggregation segments (sum, average, max, min). Using the revenue column directly without toFloat64 conversion is appropriate since it's a numeric column in the database schema, and the revenue > 0 filter ensures only relevant events are included.

apps/start/src/components/realtime/realtime-reloader.tsx (1)

17-28: LGTM!

The query filter updates expand the realtime data refresh mechanism to include referrals and paths while updating the existing liveData and activeSessions filters. The refetch pattern remains consistent with the visibility-based triggering logic.

packages/sdks/web/src/types.d.ts (1)

11-15: All revenue methods are properly implemented.

Verification confirms that revenue, flushRevenue, clearRevenue, and pendingRevenue are all implemented in the web SDK (packages/sdks/web/src/index.ts). The type definitions at lines 12-15 in packages/sdks/web/src/types.d.ts correctly match the implementations, with no gaps between declared types and actual methods.

packages/trpc/src/routers/chart.ts (2)

86-101: Revenue aggregation in metrics looks consistent.

metricsPromise now selects:

'sum(revenue * sign) as revenue',

from TABLE_NAMES.sessions over a 6‑month window. This matches the chart query’s revenue aggregation and extends the metrics shape with a revenue field, which is coherent with the rest of the PR.

No changes needed here.


217-240: Including revenue in the properties list is appropriate.

Adding 'revenue' to the static properties array ensures revenue can be used as a first‑class property in filters and property pickers:

properties.push(
  'revenue',
  'has_profile',
  'path',
  ...
);

This aligns with the new revenue column on events/sessions and with the rest of the revenue-aware charting changes.

apps/start/src/components/overview/overview-metrics.tsx (3)

79-85: Revenue metric wiring looks consistent with the rest of the UI.

The new Revenue entry in TITLES uses key total_revenue and unit 'currency', which is handled specially in the tooltip and chart rendering. This matches the backend’s total_revenue/prev_total_revenue fields and the currency formatting via useNumber.

No changes needed here.


183-252: Tooltip currency and revenue section behavior looks good.

  • Using metric.unit === 'currency' to decide between number.currency((data[metric.key] ?? 0) / 100) and formatWithUnit is correct and keeps non-currency metrics unchanged.
  • The additional revenue block conditioned on revenue > 0 cleanly surfaces total_revenue / prev_total_revenue with the same currency formatter and PreviousDiffIndicatorPure.

No changes needed here.


272-275: Using a separate Y-axis for revenue ticks is appropriate.

The revenueYAxisProps with a tickFormatter dividing by 100 to convert cents to base currency units is aligned with the tooltip and card formatting:

const revenueYAxisProps = useYAxisProps({
  tickFormatter: (value) => number.short(value / 100),
});

This makes the right-side revenue axis readable while still working with cent-based values in the data.

packages/db/prisma/migrations/20251118100123_add_revenue_tracking_setting_on_project/migration.sql (1)

1-2: Migration for allowUnsafeRevenueTracking looks correct.

Adding a BOOLEAN NOT NULL DEFAULT false column on public.projects is safe for existing data and fits the intended feature flag for unsafe revenue tracking.

No changes needed.

packages/validation/src/index.ts (1)

375-383: allowUnsafeRevenueTracking wiring into zProject looks correct

Adding allowUnsafeRevenueTracking: z.boolean().default(false) keeps projects safe by default and aligns with how the TRPC project router updates and creates projects. This cleanly exposes the flag for edits without changing existing behavior.

packages/trpc/src/routers/project.ts (1)

68-88: allowUnsafeRevenueTracking handling in update/create is consistent and safe

Update passes input.allowUnsafeRevenueTracking through (so undefined leaves the DB value unchanged), and create explicitly initializes it to false, matching the safe default in validation/DB. This cleanly supports toggling the flag later via edits while keeping onboarding projects locked down by default.

Also applies to: 120-129

apps/start/src/components/settings/edit-project-details.tsx (2)

29-29: LGTM! Field integration is consistent.

The allowUnsafeRevenueTracking field is properly added to both the validator and form defaults, following the same pattern as other boolean fields like crossDomain.

Also applies to: 43-43


178-198: LGTM! Clear security signaling with "unsafe" terminology.

The new revenue tracking section follows the established pattern and clearly communicates the security implications of enabling client-side revenue tracking.

apps/api/src/controllers/track.controller.ts (2)

119-122: LGTM! Consistent property extraction pattern.

The overrideDeviceId extraction follows the same pattern as other special properties like __ip and __timestamp, allowing the SDK to specify a deviceId when needed.


133-141: LGTM! Correct precedence for deviceId resolution.

The logic appropriately prioritizes overrideDeviceId when provided, which enables the SDK to maintain consistent device identification for revenue events.

packages/trpc/src/routers/overview.ts (2)

100-163: LGTM! Session counting logic updated consistently.

The shift from count(*) on session_start events to uniq(session_id) across all events is a semantic change that's consistently applied throughout the live data queries. This aligns with the broader session-centric refactoring in this PR.


233-233: LGTM! Revenue fields added consistently.

The prev_total_revenue fields are added following the same pattern as other previous-period metrics, maintaining consistency in the API response structure.

Also applies to: 246-246

apps/start/src/components/projects/project-chart.tsx (1)

27-85: LGTM! Tooltip implementation is clean and user-friendly.

The tooltip conditionally displays revenue only when present and formats all values appropriately using locale-aware formatters.

packages/db/prisma/schema.prisma (1)

184-184: LGTM! Secure default for revenue tracking flag.

The allowUnsafeRevenueTracking field with @default(false) ensures new projects don't allow client-side revenue tracking unless explicitly enabled.

apps/worker/src/jobs/events.incoming-event.ts (4)

44-53: LGTM! Defensive revenue parsing.

The parseRevenue function safely handles multiple input types and returns undefined for invalid values, preventing type errors downstream.


129-132: LGTM! Revenue only populated for revenue events.

The conditional logic ensures the revenue field is only set when the event is explicitly a revenue event and the __revenue property is present.


137-164: LGTM! Session-centric event enrichment for server-side events.

The migration from eventBuffer to sessionBuffer.getExistingSession provides richer context for server-side and past-timestamp events. The fallback pattern ensures events are created even when session data is unavailable.


178-191: LGTM! Correct use of exit paths for ending events.

Switching to exit_path and exit_origin from the session buffer is semantically correct for events that may occur after a user has navigated away, ensuring accurate path attribution.

apps/start/src/components/projects/project-card.tsx (2)

14-14: LGTM! Clean extraction of chart component.

The refactor to use an external ProjectChart component improves reusability while maintaining clear naming with the ProjectChartOuter wrapper.

Also applies to: 63-63, 90-90


142-153: LGTM! Improved number formatting with conditional revenue display.

The switch to the useNumber hook provides a cleaner API, and the revenue metric is appropriately displayed only when data is present. Currency formatting with short: true is well-suited for compact card layouts.

apps/worker/src/jobs/events.incoming-events.test.ts (2)

1-29: LGTM! Test mocks updated to reflect sessionBuffer migration.

The mock structure correctly reflects the production code's migration from eventBuffer.getLastScreenView to sessionBuffer.getExistingSession, maintaining comprehensive test coverage.


115-115: LGTM! Comprehensive test coverage for revenue and session data.

The test expectations correctly include revenue: undefined for non-revenue events, and the IClickhouseSession mock is detailed and realistic, ensuring thorough test coverage of the sessionBuffer integration.

Also applies to: 220-220, 289-366, 411-411

packages/db/src/services/overview.service.ts (5)

100-118: LGTM! Return type updated correctly.

The addition of total_revenue to both metrics and series structures is consistent with other numeric fields.


123-139: LGTM! Revenue aggregation follows ClickHouse patterns.

The sum(revenue * sign) aggregation is correct for ClickHouse's versioned table pattern with the sign column.


171-184: LGTM! Revenue CTEs follow existing patterns.

The overall_total_revenue and daily_stats CTEs correctly separate rollup aggregates from per-date series, consistent with the existing overall_bounce_rate pattern.


282-349: LGTM! Non-page-filter path handles revenue correctly.

This path aggregates revenue directly with sum(revenue * sign) and safely handles potential undefined values with the ?? 0 fallback on line 342.


382-633: LGTM! Revenue tracking added consistently across top queries.

The revenue field is properly integrated into getTopPages, getTopEntryExit, and getTopGeneric:

  • Consistent use of sum(revenue * sign) aggregation pattern
  • Good null-safety with coalesce(b.revenue, 0) on line 449 for the LEFT JOIN in getTopPages
  • Return types correctly updated to include revenue

Comment thread apps/api/src/utils/auth.ts
Comment on lines +59 to +96
/**
* Generates container query class based on priority.
* Lower priority numbers = hidden at smaller widths.
* Priority 1 = always visible (highest priority)
* Priority 10 = hidden first (lowest priority)
*/
function getResponsiveClass(priority: ColumnPriority): string {
// Priority 1 = always visible (no hiding)
if (priority === 1) {
return '';
}

// Columns will be hidden via CSS container queries
// Return empty string - hiding is handled by CSS
return '';
}

function getResponsiveStyle(
priority: ColumnPriority,
): React.CSSProperties | undefined {
if (priority === 1) {
return undefined;
}

const minWidth = (priority - 1) * 100 + 100;
return {
// Use CSS custom property for container query
// Will be handled by inline style with container query
} as React.CSSProperties;
}

/**
* Generates container query class based on custom min-width.
*/
function getMinWidthClass(minWidth: number): string {
return 'hidden';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

minWidth-based columns will stay permanently hidden; also some helper dead code

Two issues in the responsive helpers:

  1. Functional bug for minWidth columns

getMinWidthClass currently returns 'hidden':

function getMinWidthClass(minWidth: number): string {
  return 'hidden';
}

Since Tailwind’s hidden maps to display: none, any column using responsive.minWidth will remain invisible at all container widths: the container-query rules only toggle min-width, max-width, padding, overflow, and visibility, but never override display. So those cells won’t reappear even when the container becomes wide enough.

Given you already generate base “hidden by default, visible at @container(min-width …)” rules in containerQueryStyles, the helper should not apply hidden at all.

A minimal fix:

-function getMinWidthClass(minWidth: number): string {
-  return 'hidden';
-}
+function getMinWidthClass(minWidth: number): string {
+  // Visibility is controlled via container queries; avoid adding `hidden`
+  // (display: none), which would prevent the column from ever showing.
+  return '';
+}
  1. Redundant / unused helpers
  • getResponsiveClass always returns '', so it doesn’t currently contribute anything beyond what containerQueryStyles does. That’s fine functionally but a bit misleading given the JSDoc; consider either simplifying the comment or removing the helper once behavior stabilizes.
  • getResponsiveStyle is never used; it’s effectively dead code and could be removed to keep the file lean.
🤖 Prompt for AI Agents
In apps/start/src/components/widget-table.tsx around lines 59-96,
getMinWidthClass currently returns the string 'hidden', which causes
minWidth-based columns to be permanently display:none; change getMinWidthClass
to return an empty string (or otherwise not emit the hidden class) so that the
container-query CSS rules can control visibility, and remove or consolidate the
dead/unused helpers (getResponsiveStyle and/or getResponsiveClass) or update
their comments to reflect current behavior to keep the file lean.

Comment on lines +113 to +151
const containerId = React.useMemo(
() => `widget-table-${Math.random().toString(36).substring(7)}`,
[],
);

// Generate CSS for container queries
const containerQueryStyles = React.useMemo(() => {
const styles: string[] = [];

columns.forEach((column) => {
if (
column.responsive?.priority !== undefined &&
column.responsive.priority > 1
) {
// Breakpoints - Priority 2 = 150px, Priority 3 = 250px, etc.
// Less aggressive: columns show at smaller container widths
const minWidth = (column.responsive.priority - 1) * 100 + 50;
// Hide by default by collapsing width and hiding content
// Keep in grid flow but take up minimal space
styles.push(
`.${containerId} .cell[data-priority="${column.responsive.priority}"] { min-width: 0; max-width: 0; padding-left: 0; padding-right: 0; overflow: hidden; visibility: hidden; }`,
`@container (min-width: ${minWidth}px) { .${containerId} .cell[data-priority="${column.responsive.priority}"] { min-width: revert; max-width: revert; padding-left: revert; padding-right: 0.5rem; overflow: revert; visibility: visible !important; } }`,
);
} else if (column.responsive?.minWidth !== undefined) {
styles.push(
`.${containerId} .cell[data-min-width="${column.responsive.minWidth}"] { min-width: 0; max-width: 0; padding-left: 0; padding-right: 0; overflow: hidden; visibility: hidden; }`,
`@container (min-width: ${column.responsive.minWidth}px) { .${containerId} .cell[data-min-width="${column.responsive.minWidth}"] { min-width: revert; max-width: revert; padding-left: revert; padding-right: 0.5rem; overflow: revert; visibility: visible !important; } }`,
);
}
});

// Ensure last visible cell always has padding-right
styles.push(
`.${containerId} .cell:last-child { padding-right: 1rem !important; }`,
);

return styles.length > 0 ? <style>{styles.join('\n')}</style> : null;
}, [columns, containerId]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Random containerId risks SSR hydration mismatches; prefer a deterministic id

containerId is derived from Math.random():

const containerId = React.useMemo(
  () => `widget-table-${Math.random().toString(36).substring(7)}`,
  [],
);

In an SSR + hydration setup (which TanStack Start uses), this means the class used in the rendered HTML differs between server and client, causing hydration warnings and forcing React to patch the DOM. It also makes CSS less predictable for streaming/rehydration.

You can keep the per-instance uniqueness while making it deterministic by basing it on React.useId() and sanitizing it for CSS-class usage:

-  const containerId = React.useMemo(
-    () => `widget-table-${Math.random().toString(36).substring(7)}`,
-    [],
-  );
+  const containerId = `widget-table-${React.useId().replace(/[:]/g, '')}`;

containerQueryStyles will continue to work unchanged, since it already uses .${containerId} .cell[...] and the wrapper div applies containerId as a class.

Also applies to: 154-159


🌐 Web query:

Confirm that the React version used in this repo supports `React.useId()` (React 18+), and that existing tooling/type definitions include container-query CSS properties such as `containerType`.

💡 Result:

I don't have the repo. Please either paste the repo URL or the relevant files (package.json / package-lock.json / yarn.lock / pnpm-lock.yaml and tsconfig / any CSS type files). I can then:

  • Check the installed React version to confirm React.useId() (requires React 18+).
  • Inspect installed @types/react and other tooling (postcss, stylelint, TypeScript lib.dom types) to confirm container-query CSS properties like containerType are present.

If you prefer to check locally, paste the output of:

  • cat package.json (or share dependencies/devDependencies lines)
  • npm ls react @types/react
  • npm ls typescript
    and (if TypeScript) the value of "lib" in tsconfig.json.

I'll inspect the repository to verify the React version and tooling configuration.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check package.json for React version and dependencies
cat package.json | grep -A 50 '"dependencies"' | head -60

Length of output: 678


🏁 Script executed:

#!/bin/bash
# Check TypeScript version and tsconfig.json lib configuration
cat tsconfig.json | grep -A 5 '"lib"'

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Verify the file under review exists and check its current state around lines 113-151
head -160 apps/start/src/components/widget-table.tsx | tail -60

Length of output: 2520


I need to check the workspace-level configuration. Let me look at the apps/start package.json and tsconfig.json:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check apps/start package.json for React version
cat apps/start/package.json | grep -A 100 '"dependencies"'

Length of output: 3738


🏁 Script executed:

#!/bin/bash
# Check apps/start tsconfig.json
cat apps/start/tsconfig.json

Length of output: 642


🏁 Script executed:

#!/bin/bash
# Check for pnpm-lock.yaml to verify React version
grep -A 2 "react@" pnpm-lock.yaml | head -20

Length of output: 915


Replace Math.random()-based containerId with React.useId() for SSR compatibility

The current implementation uses Math.random() to generate containerId, which causes SSR hydration mismatches in TanStack Start (client-generated ID differs from server-rendered ID).

Replace with:

-  const containerId = React.useMemo(
-    () => `widget-table-${Math.random().toString(36).substring(7)}`,
-    [],
-  );
+  const containerId = `widget-table-${React.useId().replace(/[:]/g, '')}`;

Verification confirms React 19.1.1 is installed (supports React.useId() since React 18+), TypeScript includes DOM types with container-query properties, and the codebase already uses container queries (containerType: 'inline-size'). This refactor is fully compatible.

Also applies to: 154-159

🤖 Prompt for AI Agents
In apps/start/src/components/widget-table.tsx around lines 113 to 151 (and also
apply the same change at 154-159), replace the Math.random()-based containerId
with React.useId() to avoid SSR hydration mismatches; specifically, remove the
React.useMemo that builds a random id and instead call React.useId() (optionally
prefixing it like `widget-table-`), then use that id in the generated CSS
strings where containerId is referenced so server and client IDs match.

Comment on lines +164 to +197
{columns.map((column, colIndex) => {
const responsiveClass =
column.responsive?.priority !== undefined
? getResponsiveClass(column.responsive.priority)
: column.responsive?.minWidth !== undefined
? getMinWidthClass(column.responsive.minWidth)
: '';

const dataAttrs: Record<string, string> = {};
if (column.responsive?.priority !== undefined) {
dataAttrs['data-priority'] = String(column.responsive.priority);
}
if (column.responsive?.minWidth !== undefined) {
dataAttrs['data-min-width'] = String(column.responsive.minWidth);
}

return (
<div
key={column.name?.toString()}
className={cn(
'p-2 font-medium font-sans text-sm whitespace-nowrap cell',
columns.length > 1 && column !== columns[0]
? 'text-right'
: 'text-left',
column.className,
responsiveClass,
)}
style={{ width: column.width }}
{...dataAttrs}
>
{column.name}
</div>
);
})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Column rendering: small correctness and ergonomics tweaks (keys, alignment, width handling)

A few points in the header/body column maps that are worth tightening up:

  1. Unused colIndex / brittle alignment check

You compute colIndex but then check column !== columns[0] to decide alignment:

columns.length > 1 && column !== columns[0]
  ? 'text-right'
  : 'text-left'

For clarity (and to actually use colIndex), this is simpler and more explicit as colIndex > 0:

-                  columns.length > 1 && column !== columns[0]
-                    ? 'text-right'
-                    : 'text-left',
+                  columns.length > 1 && colIndex > 0
+                    ? 'text-right'
+                    : 'text-left',

Apply the same change in the body renderer.

  1. Keys based on column.name?.toString()

Using column.name?.toString() as the key is fragile when name is a ReactNode (e.g. an icon or element), and can lead to duplicated or unstable keys. Since column definitions are usually static, an index-based key is acceptable and more predictable here:

-                key={column.name?.toString()}
+                key={colIndex}

Likewise for the body cells.

  1. Header width handling is inconsistent with body and may be invalid for w-full

In the header you always do:

style={{ width: column.width }}

but in the body you special-case 'w-full':

column.width === 'w-full' && 'w-full min-w-0',
style={column.width !== 'w-full' ? { width: column.width } : {}}

If any callers pass Tailwind width classes like 'w-full', the header ends up with width: 'w-full', which is invalid CSS. To keep behavior consistent and avoid invalid styles, mirror the body logic in the header:

-                className={cn(
-                  'p-2 font-medium font-sans text-sm whitespace-nowrap cell',
-                  columns.length > 1 && colIndex > 0 ? 'text-right' : 'text-left',
-                  column.className,
-                  responsiveClass,
-                )}
-                style={{ width: column.width }}
+                className={cn(
+                  'p-2 font-medium font-sans text-sm whitespace-nowrap cell',
+                  columns.length > 1 && colIndex > 0 ? 'text-right' : 'text-left',
+                  column.className,
+                  column.width === 'w-full' && 'w-full min-w-0',
+                  responsiveClass,
+                )}
+                style={
+                  column.width !== 'w-full' ? { width: column.width } : {}
+                }

This keeps the layout model consistent between header and body and avoids invalid inline styles.

Also applies to: 215-255

🤖 Prompt for AI Agents
In apps/start/src/components/widget-table.tsx around lines 164-197 (and
similarly apply to 215-255), the header column rendering should (1) use the
provided colIndex for alignment checks instead of comparing object identity, (2)
use a stable index-based key (e.g., colIndex) instead of
column.name?.toString(), and (3) mirror the body width handling so Tailwind
width classes like 'w-full' are not emitted as invalid inline styles — i.e., if
column.width === 'w-full' add the class (e.g., 'w-full min-w-0') and don't set
style.width, otherwise set style.width to column.width; apply the same colIndex
alignment and key fixes to the body renderer as well.

Comment thread packages/sdks/web/src/index.ts
Comment on lines +63 to 83
const { timezone } = await getSettingsForProject(projectId);
const chartPromise = chQuery<{
value: number;
date: Date;
revenue: number;
}>(
`SELECT
uniqHLL12(profile_id) as value,
toStartOfDay(created_at) as date
toStartOfDay(created_at) as date,
sum(revenue * sign) as revenue
FROM ${TABLE_NAMES.sessions}
WHERE
sign = 1 AND
project_id = ${sqlstring.escape(projectId)} AND
created_at >= now() - interval '3 month'
GROUP BY date
ORDER BY date ASC
WITH FILL FROM toStartOfDay(now() - interval '1 month')
TO toStartOfDay(now())
STEP INTERVAL 1 day
SETTINGS session_timezone = '${timezone}'
`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Fix ClickHouse interval syntax and escape timezone.

The raw SQL in projectCard looks like Postgres-style intervals:

created_at >= now() - interval '3 month'
...
WITH FILL FROM toStartOfDay(now() - interval '1 month')

ClickHouse expects INTERVAL without quotes, e.g. now() - INTERVAL 3 MONTH. As written, this query is very likely to fail at runtime.

Also, session_timezone = '${timezone}' interpolates a DB value directly into SQL; use sqlstring.escape to avoid injection from a misconfigured timezone string.

Suggested fix:

-            created_at >= now() - interval '3 month'
+            created_at >= now() - INTERVAL 3 MONTH
...
-        WITH FILL FROM toStartOfDay(now() - interval '1 month') 
-        TO toStartOfDay(now()) 
+        WITH FILL FROM toStartOfDay(now() - INTERVAL 1 MONTH) 
+        TO toStartOfDay(now()) 
...
-        SETTINGS session_timezone = '${timezone}'
+        SETTINGS session_timezone = ${sqlstring.escape(timezone)}

This should make the query valid for ClickHouse and safer.


I'll verify the review comment by examining the actual code and checking ClickHouse documentation.


🏁 Script executed:

#!/bin/bash
# Examine the file to see the actual code at the specified lines
if [ -f "packages/trpc/src/routers/chart.ts" ]; then
  echo "=== File content (lines 60-90) ==="
  sed -n '60,90p' packages/trpc/src/routers/chart.ts
else
  echo "File not found at packages/trpc/src/routers/chart.ts"
  find . -name "chart.ts" -type f | head -5
fi

Length of output: 990


🌐 Web query:

ClickHouse INTERVAL syntax correct format

💡 Result:

Short answer — valid ClickHouse INTERVAL forms

  • Keyword form: INTERVAL

    • Example: INTERVAL 4 DAY, INTERVAL 10 HOUR. [1]
  • Quoted/string form: INTERVAL '' or INTERVAL ''

    • Examples: INTERVAL '1' hour, INTERVAL '1 hour', INTERVAL '1 HOUR 30 MINUTE' (composite). [1][3]
  • Programmatic constructors (equivalent): toIntervalSecond(n), toIntervalMinute(n), toIntervalHour(n), toIntervalDay(n), toIntervalWeek(n), toIntervalMonth(n), toIntervalQuarter(n), toIntervalYear(n). Use these when you need explicit Interval-typed values. Example: toIntervalMonth(1). [2]

Notes

  • Different interval types cannot be combined directly without using the composite/tuple syntax; use separate INTERVAL terms added together or the quoted composite form / tuple of intervals. [1][3]

Sources: ClickHouse docs — INTERVAL/operator and date-time functions. [1][2][3]


Escape timezone parameter to prevent SQL injection.

The session_timezone setting interpolates the timezone value directly without escaping: ClickHouse supports both INTERVAL 3 MONTH (unquoted) and INTERVAL '3 month' (quoted) forms, so the current interval syntax is valid. However, the timezone should be escaped using sqlstring.escape() to mitigate potential SQL injection if the timezone value is unexpectedly malformed.

Suggested fix:

-        SETTINGS session_timezone = '${timezone}'
+        SETTINGS session_timezone = ${sqlstring.escape(timezone)}
🤖 Prompt for AI Agents
In packages/trpc/src/routers/chart.ts around lines 63 to 83, the
session_timezone setting interpolates the timezone string directly into the SQL
which risks SQL injection; escape the timezone before interpolation (e.g., pass
timezone through sqlstring.escape or equivalent and use the escaped value in the
SETTINGS clause) so the constructed query uses a properly quoted/escaped
timezone value.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
apps/api/src/controllers/track.controller.ts (1)

433-433: Fix incorrect error log message.

The error log references /profile but should reference /device-id or /track/device-id as this is the actual endpoint.

Based on past review comments.

Apply this diff:

- request.log.error('Error getting session end GET /track/device-id', error);
+ request.log.error('Error checking session existence for GET /track/device-id', error);
🧹 Nitpick comments (1)
apps/worker/src/jobs/events.create-session-end.ts (1)

45-62: Session retrieval correctly updated to match new API.

The call now properly passes only { sessionId } instead of both projectId and sessionId, resolving the previous critical signature mismatch. The transformation via transformSessionToEvent and null filtering with flatMap are correct.

Minor point: The variable lastScreenView is misleading since it now holds a session object rather than a screen view event.

Consider renaming for clarity:

-  const [lastScreenView, eventsInDb] = await Promise.all([
+  const [session, eventsInDb] = await Promise.all([
     sessionBuffer.getExistingSession({
       sessionId,
     }),
     getEvents(sql),
   ]);
 
   // sort last inserted first
   return [
-    lastScreenView ? transformSessionToEvent(lastScreenView) : null,
+    session ? transformSessionToEvent(session) : null,
     ...eventsInDb,
   ]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04d5b0e and a32fa73.

📒 Files selected for processing (7)
  • apps/api/src/controllers/track.controller.ts (4 hunks)
  • apps/api/src/utils/auth.ts (1 hunks)
  • apps/public/content/docs/get-started/revenue-tracking.mdx (1 hunks)
  • apps/worker/src/jobs/events.create-session-end.ts (5 hunks)
  • packages/db/src/buffers/session-buffer.ts (5 hunks)
  • packages/sdks/sdk/src/index.ts (1 hunks)
  • packages/sdks/web/src/index.ts (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/public/content/docs/get-started/revenue-tracking.mdx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/sdks/sdk/src/index.ts
  • packages/sdks/web/src/index.ts
  • apps/api/src/utils/auth.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/api/src/controllers/track.controller.ts (3)
packages/sdks/sdk/src/index.ts (1)
  • fetchDeviceId (199-206)
packages/db/src/services/salt.service.ts (1)
  • getSalts (20-49)
packages/redis/redis.ts (1)
  • getRedisCache (66-72)
apps/worker/src/jobs/events.create-session-end.ts (3)
packages/db/src/services/event.service.ts (3)
  • IServiceEvent (198-234)
  • getEvents (310-353)
  • transformSessionToEvent (103-150)
packages/db/src/buffers/index.ts (1)
  • sessionBuffer (9-9)
packages/db/src/clickhouse/client.ts (1)
  • convertClickhouseDateToJs (294-296)
⏰ 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). (3)
  • GitHub Check: build-and-push-worker
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-api
🔇 Additional comments (6)
packages/db/src/buffers/session-buffer.ts (3)

31-55: LGTM! Well-designed overload for dual-key session lookups.

The discriminated union correctly enforces mutually exclusive lookup patterns (by sessionId OR by projectId+profileId), and the implementation properly dispatches to the appropriate Redis key. This enables both direct session lookups and profile-scoped session retrieval.


98-100: Revenue accumulation correctly handles legacy sessions.

The explicit ?? 0 defaults prevent NaN when newSession.revenue is undefined for sessions cached before this rollout. This directly addresses the previous critical issue.


197-204: Profile-scoped cache correctly guarded.

The if (newSession.profile_id) guard prevents writing to malformed keys like session:${projectId}:undefined for anonymous users. This resolves the previous major issue regarding session collision.

apps/worker/src/jobs/events.create-session-end.ts (1)

95-109: Session property access is safe and correct.

The event creation properly sources data from the session object (properties, exit_path, ended_at, profile_id) with appropriate null handling. The null check on lines 79-81 ensures session is non-null for all subsequent accesses.

apps/api/src/controllers/track.controller.ts (2)

9-9: LGTM!

The import is correctly added and used in the new fetchDeviceId function.


413-434: The error is already logged at ERROR level; this review mischaracterizes the implementation.

The catch block uses request.log.error(), which logs at ERROR severity—not a lower level. The error is not masked; it's explicitly captured and logged. Additionally, after the error is logged, the function gracefully returns a 200 status with a default currentDeviceId, which appears to be the intended resilience strategy for this endpoint (device ID lookup can continue safely without Redis data).

This pattern—logging errors and gracefully degrading—is consistent across the codebase (e.g., session-consistency.ts, base-buffer.ts). While system-wide metrics/monitoring integration would improve observability, that's not a specific actionable fix for this code, and the review does not identify an actual critical failure being masked.

Likely an incorrect or invalid review comment.

Comment thread apps/api/src/controllers/track.controller.ts Outdated
Comment thread apps/api/src/controllers/track.controller.ts Outdated
Comment thread apps/api/src/controllers/track.controller.ts Outdated
Comment thread apps/api/src/controllers/track.controller.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/start/src/components/report-chart/common/report-table.tsx (1)

22-22: Remove unused import.

The logDependencies import from mathjs is not used anywhere in this file.

Apply this diff:

-import { logDependencies } from 'mathjs';
🧹 Nitpick comments (4)
apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx (1)

8-25: Profile events tab head/meta wiring looks good

The new title import and head configuration correctly use createProjectTitle(PAGE_TITLES.PROFILE_EVENTS) and align with the pattern in the profile details tab. If you later have easy access to project/organization names at this level, you could optionally pass them into createProjectTitle for more descriptive titles, but this is not required for correctness.

apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.index.tsx (1)

10-56: Profile details tab title configuration is consistent and correct

Using createProjectTitle(PAGE_TITLES.PROFILE_DETAILS) in head matches the new title helper pattern and keeps route titles consistent across profile tabs. As an optional enhancement, if you later surface projectName / organizationName here, passing them into createProjectTitle would make the document title even clearer.

apps/start/src/components/overview/overview-widget-table.tsx (1)

11-46: Consider extracting the hardcoded revenue color to a design token.

The revenue color #3ba974 is hardcoded in both the SVG (line 36) and the text styling (lines 246, 417). For consistency and maintainability, consider extracting this to a CSS variable or design token that can be referenced throughout the application.

Example approach:

      <circle
        cx={size / 2}
        cy={size / 2}
        r={radius}
        fill="none"
-       stroke="#3ba974"
+       stroke="var(--color-revenue)"
        strokeWidth={strokeWidth}
        strokeDasharray={circumference}
        strokeDashoffset={offset}
        strokeLinecap="round"
        transform={`rotate(-90 ${size / 2} ${size / 2})`}
        className="transition-all"
      />
apps/start/src/components/profiles/profile-metrics.tsx (1)

9-101: Hide-on-zero behavior and Revenue metric wiring look correct

The hideOnZero flag combined with the filter correctly hides only metrics that explicitly opt in (currently Revenue) when their value is exactly 0, while leaving all other metrics unaffected. The new Revenue definition is consistent with IProfileMetrics.revenue and the 'currency' unit used downstream.

If you want to tighten things up later, you could slightly simplify the filter:

PROFILE_METRICS.filter(
  (metric) => !(metric.hideOnZero && data[metric.key] === 0),
)

but the current implementation is already clear and correct.

Also applies to: 107-112

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a32fa73 and 526722b.

📒 Files selected for processing (13)
  • apps/start/src/components/overview/overview-metric-card.tsx (6 hunks)
  • apps/start/src/components/overview/overview-metrics.tsx (17 hunks)
  • apps/start/src/components/overview/overview-widget-table.tsx (7 hunks)
  • apps/start/src/components/profiles/profile-metrics.tsx (1 hunks)
  • apps/start/src/components/report-chart/common/axis.tsx (2 hunks)
  • apps/start/src/components/report-chart/common/report-chart-tooltip.tsx (1 hunks)
  • apps/start/src/components/report-chart/common/report-table.tsx (1 hunks)
  • apps/start/src/components/report-chart/conversion/chart.tsx (2 hunks)
  • apps/start/src/hooks/use-format-date-interval.ts (2 hunks)
  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx (2 hunks)
  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.index.tsx (2 hunks)
  • apps/start/src/utils/title.ts (1 hunks)
  • packages/db/src/services/profile.service.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
apps/start/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/start/.cursorrules)

apps/start/**/*.{ts,tsx}: Instrument server functions created with createServerFn by wrapping their implementation in Sentry.startSpan and importing Sentry from '@sentry/tanstackstart-react'
When using Sentry in the TanStack Start React app, import it as: import * as Sentry from '@sentry/tanstackstart-react'

Files:

  • apps/start/src/components/report-chart/common/report-chart-tooltip.tsx
  • apps/start/src/components/report-chart/conversion/chart.tsx
  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx
  • apps/start/src/components/report-chart/common/axis.tsx
  • apps/start/src/utils/title.ts
  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.index.tsx
  • apps/start/src/components/report-chart/common/report-table.tsx
  • apps/start/src/hooks/use-format-date-interval.ts
  • apps/start/src/components/overview/overview-metrics.tsx
  • apps/start/src/components/overview/overview-metric-card.tsx
  • apps/start/src/components/overview/overview-widget-table.tsx
  • apps/start/src/components/profiles/profile-metrics.tsx
🧠 Learnings (3)
📚 Learning: 2025-10-16T10:29:28.924Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: apps/start/.cursorrules:0-0
Timestamp: 2025-10-16T10:29:28.924Z
Learning: Applies to apps/start/routes/**/*.tsx : Define React route files with createFileRoute and let the bundler/CLI manage the path string

Applied to files:

  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx
📚 Learning: 2025-10-16T10:29:28.924Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: apps/start/.cursorrules:0-0
Timestamp: 2025-10-16T10:29:28.924Z
Learning: Applies to apps/start/routes/**/{index,*.index}.tsx : For index routes, use index.tsx or *.index.tsx files and ensure the createFileRoute path uses a trailing slash (e.g., '/posts/')

Applied to files:

  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx
📚 Learning: 2025-10-16T10:29:28.924Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: apps/start/.cursorrules:0-0
Timestamp: 2025-10-16T10:29:28.924Z
Learning: Applies to apps/start/routes/**/_*.tsx : Use leading underscore filenames for pathless layout routes that wrap children without matching a URL segment

Applied to files:

  • apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx
🧬 Code graph analysis (11)
apps/start/src/components/report-chart/common/report-chart-tooltip.tsx (1)
apps/start/src/hooks/use-format-date-interval.ts (1)
  • useFormatDateInterval (49-58)
packages/db/src/services/profile.service.ts (1)
packages/db/src/clickhouse/client.ts (1)
  • TABLE_NAMES (49-61)
apps/start/src/components/report-chart/conversion/chart.tsx (2)
apps/start/src/utils/date.ts (1)
  • formatDate (19-27)
apps/start/src/hooks/use-format-date-interval.ts (1)
  • useFormatDateInterval (49-58)
apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.events.tsx (1)
apps/start/src/utils/title.ts (2)
  • createProjectTitle (39-53)
  • PAGE_TITLES (78-128)
apps/start/src/components/report-chart/common/axis.tsx (2)
apps/start/src/utils/date.ts (1)
  • formatDate (19-27)
apps/start/src/hooks/use-format-date-interval.ts (1)
  • useFormatDateInterval (49-58)
apps/start/src/routes/_app.$organizationId.$projectId.profiles.$profileId._tabs.index.tsx (1)
apps/start/src/utils/title.ts (2)
  • createProjectTitle (39-53)
  • PAGE_TITLES (78-128)
apps/start/src/components/report-chart/common/report-table.tsx (1)
apps/start/src/hooks/use-format-date-interval.ts (1)
  • useFormatDateInterval (49-58)
apps/start/src/hooks/use-format-date-interval.ts (1)
packages/validation/src/types.validation.ts (1)
  • IInterval (34-34)
apps/start/src/components/overview/overview-metrics.tsx (6)
packages/validation/src/types.validation.ts (1)
  • IInterval (34-34)
apps/start/src/hooks/use-format-date-interval.ts (1)
  • useFormatDateInterval (49-58)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/theme.ts (1)
  • getChartColor (25-27)
packages/common/src/get-previous-metric.ts (1)
  • getPreviousMetric (7-39)
apps/start/src/components/report-chart/common/axis.tsx (1)
  • useYAxisProps (26-51)
apps/start/src/components/overview/overview-metric-card.tsx (3)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
apps/start/src/utils/date.ts (1)
  • formatDate (19-27)
apps/public/components/ui/tooltip.tsx (1)
  • Tooltiper (36-51)
apps/start/src/components/overview/overview-widget-table.tsx (1)
apps/start/src/hooks/use-numer-formatter.ts (1)
  • useNumber (73-108)
⏰ 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). (3)
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-worker
  • GitHub Check: build-and-push-api
🔇 Additional comments (14)
apps/start/src/utils/title.ts (1)

93-96: PAGE_TITLES profile entries added cleanly

The new PROFILES, PROFILE_EVENTS, and PROFILE_DETAILS keys fit the existing PAGE_TITLES structure and line up with their usage in the profile routes. No issues from a typing or behavior perspective.

apps/start/src/components/report-chart/common/axis.tsx (1)

73-76: LGTM!

The date formatting correctly adopts the new options object signature with short: true for concise axis labels.

apps/start/src/components/report-chart/common/report-chart-tooltip.tsx (1)

65-68: LGTM!

The date formatting correctly adopts the new options object signature with short: false for full date display in tooltips.

apps/start/src/components/report-chart/conversion/chart.tsx (2)

1-1: LGTM!

The chart click handler is properly implemented with useCallback, extracts the date from the clicked data point, and opens the AddReference modal. The integration is clean and follows React best practices.

Also applies to: 5-5, 80-89, 95-95


174-177: LGTM!

The date formatting correctly adopts the new options object signature with short: false for full date display in the conversion tooltip.

apps/start/src/components/report-chart/common/report-table.tsx (1)

43-46: LGTM!

The date formatting correctly adopts the new options object signature with short: true for concise table column headers.

apps/start/src/hooks/use-format-date-interval.ts (2)

3-20: LGTM!

The refactor to an options object is clean and the short flag logic is correct: when short is false for hour/minute intervals, it includes month and day for context, otherwise just shows time. This provides appropriate formatting for both compact axis labels and verbose tooltips.


49-57: LGTM!

The hook correctly forwards the options object to formatDateInterval and properly normalizes string/Date inputs. The refactor maintains a clean, composable API.

apps/start/src/components/overview/overview-widget-table.tsx (3)

233-258: Revenue column implementation looks good.

The conditional rendering based on hasRevenue prevents displaying empty revenue columns, and the division by 100 correctly converts from cents to currency units. The percentage calculation is properly guarded against division by zero.


176-262: No issues found — responsive priorities are correctly assigned.

The WidgetTable component documentation confirms that lower priority numbers indicate higher visibility priority (Path: 1 always visible, Sessions: 2, Revenue: 3, BR: 6, Duration: 7 hidden at smaller widths). The responsive priority assignments in overview-widget-table.tsx follow this semantic correctly, with the most important columns prioritized first.


419-420: The currency formatter supports the options parameter—no changes needed.

Verification confirms that the currency() function returned by formatCurrency accepts an optional options parameter with a short property. Line 420's call to number.currency(revenue / 100, { short: true }) is correct and consistent with the API, and similar patterns are already used elsewhere in the codebase (e.g., apps/start/src/components/projects/project-card.tsx:145).

packages/db/src/services/profile.service.ts (1)

18-32: Revenue metric plumbing is consistent; confirm stored unit (cents vs. major currency)

Adding revenue: number to IProfileMetrics and sourcing it via the new revenue CTE (summing events.revenue where name = 'revenue') lines up with how the UI expects to consume it, and the ClickHouse CTE structure matches the existing pattern.

The front end is formatting revenue by dividing by 100 before passing values into number.currency, which implies that events.revenue is stored in cents (or another smallest currency unit). Please double‑check that the column and ingestion logic indeed use that convention; if the DB stores major units already, the / 100 in the UI should be dropped instead.

Also applies to: 80-83, 96-97

apps/start/src/components/overview/overview-metrics.tsx (2)

41-84: Revenue title and per-metric data wiring are consistent

Adding the Revenue entry to TITLES with key total_revenue and unit 'currency', and feeding OverviewMetricCard with { date, current: item[title.key], previous: item[\prev_${title.key}`] }` keeps the overview metrics panel aligned with the stats API shape.

This sets up the card correctly for both the main value and the per-point sparkline/tooltip behavior for revenue (in cents, formatted as currency in the card).

Also applies to: 114-133


166-173: Tooltip currency formatting and Revenue block behavior are coherent

The tooltip implementation:

  • Uses useFormatDateInterval with the provided interval for the header date.
  • Correctly formats currency metrics as number.currency((data[metric.key] ?? 0) / 100), and does the same for previous values.
  • Adds a separate Revenue section when anyMetric && revenue > 0, formatting both current and previous revenue in dollars and feeding them into PreviousDiffIndicatorPure via getPreviousMetric.

This matches the rest of the app’s “cents in data, divide by 100 in UI” convention and cleanly separates the active metric from the revenue overlay.

Please just confirm the backend always supplies total_revenue/prev_total_revenue in cents so these / 100 conversions stay correct across all charts.

Also applies to: 175-179, 185-187, 197-197, 203-216, 229-254

Comment thread apps/start/src/components/overview/overview-metric-card.tsx
Comment thread apps/start/src/components/overview/overview-metrics.tsx
Comment thread apps/start/src/components/report-chart/common/axis.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/db/code-migrations/6-add-revenue-column.ts (2)

9-18: Document the revenue data type and units.

The migration adds a revenue column with type UInt64, which is appropriate for storing integer values. However, the units and precision are not documented:

  • If revenue is stored in cents (or smallest currency unit), this should be documented in a comment
  • If revenue needs to support decimal values (e.g., $10.99), UInt64 won't work and you'd need Decimal64(2) or similar
  • The column will default to 0 for existing rows, which is likely correct but worth confirming

Consider adding a comment to clarify:

  const sqls: string[] = [
    ...addColumns(
      'events',
+     // Revenue stored as integer in smallest currency unit (e.g., cents for USD)
      ['`revenue` UInt64 AFTER `referrer_type`'],
      isClustered,
    ),
  ];

33-35: LGTM! Consider adding a rollback function.

The migration execution logic is correct and includes dry-run support, which is excellent for testing.

However, consider adding a down() function to support rollback:

export async function down() {
  const isClustered = getIsCluster();
  
  const sqls: string[] = dropColumns('events', ['revenue'], isClustered);
  
  if (!process.argv.includes('--dry')) {
    await runClickhouseMigrationCommands(sqls);
  }
}

This would allow reverting the migration if needed during development or troubleshooting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 526722b and 85f87e2.

📒 Files selected for processing (5)
  • .gitignore (1 hunks)
  • packages/db/code-migrations/3-init-ch.sql (0 hunks)
  • packages/db/code-migrations/5-add-imports-table.sql (0 hunks)
  • packages/db/code-migrations/6-add-revenue-column.ts (1 hunks)
  • packages/db/src/clickhouse/migration.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/db/code-migrations/5-add-imports-table.sql
  • packages/db/code-migrations/3-init-ch.sql
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-26T18:30:17.476Z
Learnt from: CR
Repo: Openpanel-dev/openpanel PR: 0
File: .cursorrules:0-0
Timestamp: 2025-09-26T18:30:17.476Z
Learning: When writing ClickHouse queries, use the helper functions in ./packages/db/src/clickhouse/query-functions.ts rather than crafting raw queries

Applied to files:

  • packages/db/code-migrations/6-add-revenue-column.ts
🧬 Code graph analysis (1)
packages/db/code-migrations/6-add-revenue-column.ts (2)
packages/db/code-migrations/helpers.ts (1)
  • getIsCluster (17-24)
packages/db/src/clickhouse/migration.ts (2)
  • addColumns (137-152)
  • runClickhouseMigrationCommands (338-470)
⏰ 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). (3)
  • GitHub Check: build-and-push-dashboard
  • GitHub Check: build-and-push-api
  • GitHub Check: build-and-push-worker
🔇 Additional comments (1)
packages/db/code-migrations/6-add-revenue-column.ts (1)

20-31: LGTM!

Writing the SQL statements to a .sql file is good practice for documentation and debugging. The formatting logic correctly normalizes the statements and uses a clear delimiter.

Comment thread packages/db/src/clickhouse/migration.ts
@lindesvard lindesvard merged commit 790801b into main Nov 19, 2025
8 of 9 checks passed
@lindesvard lindesvard deleted the feature/revenue-2 branch November 19, 2025 13:27
@coderabbitai coderabbitai Bot mentioned this pull request Nov 19, 2025
@coderabbitai coderabbitai Bot mentioned this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant