Skip to content

Comments

feat: Revenue analytics#454

Merged
Blaumaus merged 20 commits intoswetrix-revampfrom
revenue-analytics
Dec 19, 2025
Merged

feat: Revenue analytics#454
Blaumaus merged 20 commits intoswetrix-revampfrom
revenue-analytics

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Dec 17, 2025

Changes

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

Release Notes

  • New Features

    • Added revenue tracking integration with Stripe and Paddle payment providers
    • Revenue metrics now display in dashboards, traffic charts, and session details
    • Support for tracking sales and refunds in user activity pageflow
    • New Revenue section in project settings for API key configuration and currency management
    • Revenue statistics including total revenue, sales count, refunds, and average order value
  • Enhancements

    • Projects can now be pinned and automatically prioritized in lists
    • Session and profile views now display associated revenue data with currency formatting
    • Analytics enriched with revenue and refund breakdowns
  • Documentation

    • Updated translations and UI labels for revenue-related features

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces a comprehensive revenue tracking system integrating Paddle and Stripe payment providers, enabling transaction synchronization, revenue analytics, and encrypted API key storage. Adds new backend services, controllers, and adapters; extends analytics to include revenue data in profiles and sessions; updates database schemas; and provides frontend UI for revenue management and data visualization.

Changes

Cohort / File(s) Summary
Revenue System Core
backend/apps/cloud/src/revenue/revenue.service.ts, backend/apps/cloud/src/revenue/revenue.controller.ts, backend/apps/cloud/src/revenue/revenue.module.ts
Core revenue management service with encrypted API key storage, provider connection/disconnection, statistics, charting, transactions retrieval, and ClickHouse integration. RevenueController and RevenueAnalyticsController expose endpoints for status, provider management, currency updates, sync operations, and analytics queries. Module wires dependencies with forwardRef for circular imports.
Revenue Adapters
backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts, backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts
Adapter classes for Paddle and Stripe API integration; validate API keys, fetch transactions/adjustments/refunds, process into internal RevenueTransaction format, handle currency conversion, and persist via RevenueService.
Revenue DTOs
backend/apps/cloud/src/revenue/dto/payment-log.dto.ts, backend/apps/cloud/src/revenue/dto/connect-paddle.dto.ts, backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts, backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts, backend/apps/cloud/src/revenue/dto/revenue-stats.dto.ts
Data transfer objects for payment logging, provider connection, revenue stats, transactions, breakdowns, and filtering with validation and Swagger metadata.
Revenue Interfaces
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts
Domain models including enums (RevenueProvider, RevenueType, RevenueStatus), interfaces for transactions, stats, charts, breakdowns, and permission constants for Paddle/Stripe.
Analytics Integration
backend/apps/cloud/src/analytics/analytics.controller.ts, backend/apps/cloud/src/analytics/analytics.module.ts, backend/apps/cloud/src/analytics/analytics.service.ts, backend/apps/cloud/src/analytics/interfaces/index.ts
New analytics endpoints for profile/session/payment logging; RevenueService injected; analytics queries augmented with revenue totals CTEs; pageflow type union expanded to include 'error', 'sale', 'refund'; processPageflow returns amount/currency for sales/refunds.
Project Entity & Service
backend/apps/cloud/src/project/entity/project.entity.ts, backend/apps/cloud/src/project/project.service.ts
Project entity extended with paddleApiKeyEnc, stripeApiKeyEnc, revenueCurrency, permission fields, and sync timestamps. Project service pagination query refactored to compute isPinned via database join and apply pinned-first sorting.
App & Task Manager Setup
backend/apps/cloud/src/app.module.ts, backend/apps/cloud/src/task-manager/task-manager.module.ts, backend/apps/cloud/src/task-manager/task-manager.service.ts
RevenueModule imported into AppModule and TaskManagerModule. TaskManagerService injected with RevenueService and PaddleAdapter; syncRevenueData cron method added to sync Paddle transactions every 30 minutes.
Database Migrations
backend/migrations/clickhouse/2025_12_15_revenue.js, backend/migrations/clickhouse/initialise_database.js, backend/migrations/mysql/2025_12_15_revenue.sql, backend/migrations/mysql/2025_12_18_revenue_stripe.sql
New ClickHouse revenue table with transaction fields, provider, type, status, amounts, currencies, metadata, timestamps, using ReplacingMergeTree engine. MySQL migrations add paddleApiKeyEnc, stripeApiKeyEnc, revenueCurrency, permissions, sync timestamp to project table.
Frontend API & Models
web/app/api/index.ts, web/app/lib/models/Project.ts
New API surface for revenue status, connection, disconnection, syncing, currency updates, and data retrieval. Session model extended with revenue/refunds; ProfileDetails extended with totalRevenue/revenueCurrency.
Frontend Revenue UI
web/app/pages/Project/Settings/tabs/Revenue.tsx
New Revenue component managing provider connections (Stripe/Paddle), API key input, currency selection, disconnect, and currency updates with success/error notifications.
Frontend Views & Components
web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/pages/Project/Profiles/View/ProfilesView.tsx, web/app/pages/Project/Sessions/View/SessionsView.tsx, web/app/pages/Project/Traffic/View/TrafficView.tsx, web/app/pages/Project/View/ViewProject.helpers.tsx, web/app/pages/Project/View/components/Pageflow.tsx, web/app/pages/Project/View/components/Sessions.tsx, web/app/pages/Project/View/components/UserDetails.tsx
Revenue tab added to ProjectSettings. Currency prop threaded through Profiles/Sessions/UserDetails views. TrafficView fetches revenue chart data and displays revenue/refunds metrics. ViewProject.helpers extended with revenue series configuration (bars, colors, stacking, tooltip). Pageflow component accepts amount/currency, renders sale/refund events with currency formatting. Sessions component displays revenue with conditional coloring.
Styling & Icons
web/app/styles/ProjectViewStyle.css, web/app/ui/icons/Paddle.tsx, web/app/ui/icons/Stripe.tsx
New CSS rules for revenue and refundsAmount bar styling. New PaddleSVG and StripeSVG icon components.
Community Utils & Locales
backend/apps/community/src/common/utils.ts, backend/apps/community/src/project/project.controller.ts, web/public/locales/en.json, .claude/frontend-design.md
Community utils refactored to compute isPinned and apply pinned-first sorting for projects. Project controller updated to retrieve and mark pinned projects. Translation file expanded with revenue tracking, currency, sync status, and related labels. Frontend design philosophy doc added.
DangerZone & Misc
web/app/pages/Project/View/Panels.tsx, web/app/pages/Project/Settings/tabs/DangerZone.tsx, web/app/pages/Project/View/ViewProject.tsx
Panel headers changed to bg-transparent. DangerZone translation calls simplified (removed defaultValue). ViewProject comment noting revenue feature placement.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser
    participant RevenueController
    participant RevenueService
    participant PaddleAdapter
    participant PaddleAPI
    participant ClickHouse
    participant Project DB
    
    User->>Browser: Connect Paddle (API key + currency)
    Browser->>RevenueController: POST /connect-revenue
    RevenueController->>PaddleAdapter: validateApiKey(apiKey)
    PaddleAdapter->>PaddleAPI: Test API call
    PaddleAPI-->>PaddleAdapter: ✓ Valid
    RevenueController->>RevenueService: connectPaddle(projectId, key, currency)
    RevenueService->>RevenueService: encrypt(apiKey)
    RevenueService->>Project DB: Update project with<br/>paddleApiKeyEnc, currency
    Project DB-->>RevenueService: ✓
    RevenueController-->>Browser: { success: true }
    
    Note over Browser,ClickHouse: Scheduled Sync (every 30 min)
    
    participant TaskManager
    TaskManager->>TaskManager: cron trigger
    TaskManager->>RevenueService: getPaddleApiKey(projectId)
    RevenueService-->>TaskManager: decrypted apiKey
    TaskManager->>PaddleAdapter: syncTransactions(projectId, key, currency)
    PaddleAdapter->>PaddleAPI: Fetch transactions (paginated)
    PaddleAPI-->>PaddleAdapter: Transaction list
    PaddleAdapter->>PaddleAdapter: Process & map to<br/>RevenueTransaction
    PaddleAdapter->>RevenueService: insertTransaction(tx)
    RevenueService->>ClickHouse: INSERT INTO revenue
    ClickHouse-->>RevenueService: ✓
    RevenueService->>Project DB: Update revenueLastSyncAt
    PaddleAdapter-->>TaskManager: ✓ Sync complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • Encryption/Key Management (revenue.service.ts connectPaddle, connectStripe, getPaddleApiKey, getStripeApiKey): Verify AES-256-CBC encryption implementation, key derivation from JWT_ACCESS_TOKEN_SECRET, and decrypt logic correctness; ensure no plaintext API keys are logged or exposed.
  • Payment Processing Logic (paddle.adapter.ts, stripe.adapter.ts): Complex currency conversion, transaction mapping, adjustment/refund handling, and pagination logic; verify correctness of type/status derivation, metadata extraction, and amount calculations.
  • Analytics Query Modifications (analytics.service.ts): Revenue totals CTE joins multiple tables; verify correctness of union logic, grouping, casting (toString vs CAST), and revenue/refund aggregations across sale/refund page types.
  • Project Pinning Refactor (project.service.ts, backend/apps/community/src/common/utils.ts, project.controller.ts): Database-level sorting logic now handles isPinned; verify consistency between QueryBuilder order-by and community utils implementation.
  • Frontend Revenue Display (ViewProject.helpers.tsx, TrafficView.tsx, Pageflow.tsx, Sessions.tsx): Verify alignment of revenue chart data to main chart x-axis, currency formatting, stacking logic, and conditional rendering based on self-hosted status.
  • Database Migrations: Review ClickHouse table schema, ReplacingMergeTree configuration, partition/order keys; verify MySQL column additions don't conflict with existing schema.
  • Task Manager Sync (task-manager.service.ts): Verify Promise.allSettled error handling doesn't swallow critical failures; ensure per-project sync failures don't block other projects.

Possibly related PRs

  • Revive captcha #443: Modifies backend/apps/cloud/src/project/entity/project.entity.ts with captcha-related columns—potential schema conflict or interaction with revenue field additions.
  • Country blacklist project setting #427: Modifies analytics.controller.ts and analytics.service.ts—overlapping changes to analytics subsystem; verify no regression in existing profile/session endpoints.
  • Profiles / MAU #425: Modifies analytics around profile/session ID generation and endpoints (same classes as this PR)—potential logic duplication or conflict in ID generation flows.

Poem

🐰 Revenue flows like clover in spring,
Paddle and Stripe dancing on wing,
Transactions sync'd in the ClickHouse night,
With currency conversions done right,
Sales and refunds now gleam in plain sight! 💰

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description follows the required template but is mostly a checkbox-only response with minimal substantive content about the revenue analytics feature. Expand the description with details about the revenue analytics implementation, including which payment providers are supported (Paddle/Stripe), how revenue data is tracked, and key features being added.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Revenue analytics' clearly summarizes the main change, indicating a new feature for revenue analytics functionality across the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@Blaumaus Blaumaus marked this pull request as ready for review December 19, 2025 01:14
@Blaumaus
Copy link
Member Author

@CodeRabbit please review

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

@Blaumaus: I'll review the changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (1)
backend/apps/cloud/src/analytics/interfaces/index.ts (1)

183-188: Fix type safety mismatch in processPageflow return type.

The processPageflow() method in analytics.service.ts returns objects with amount and currency fields for 'sale' and 'refund' event types (extracted from metadata), but the method has no explicit return type annotation. This creates a type safety gap: the actual return type doesn't match the IPageflow interface signature.

For sale/refund events, the method extracts amount and currency from the metadata array and adds them as explicit object properties. However, IPageflow doesn't declare these fields, causing a contract violation between the declared input type and actual output structure.

Recommendation: Define an explicit return type for processPageflow() that accounts for the optional amount and currency fields on sale/refund events, or extend IPageflow to include these fields as optional properties.

🧹 Nitpick comments (31)
backend/apps/community/src/project/project.controller.ts (1)

201-228: Consider using a Set for O(1) lookups in the sort comparator.

The _includes(pinnedProjectIds, ...) call inside the sort comparator runs O(n log n) times, each with O(m) array lookup. For better performance, convert pinnedProjectIds to a Set before sorting.

🔎 Proposed optimization
     // Get pinned project IDs to mark shared projects as pinned too
     const pinnedProjectIds = await getPinnedProjectsClickhouse(userId)
+    const pinnedProjectIdsSet = new Set(pinnedProjectIds)

     // Always sort by pinned first, then by the requested sort criteria
     combinedProjects = [...combinedProjects].sort((a: any, b: any) => {
       // First, sort by pinned status (pinned projects first)
-      const aIsPinned = a.isPinned || _includes(pinnedProjectIds, a.id)
-      const bIsPinned = b.isPinned || _includes(pinnedProjectIds, b.id)
+      const aIsPinned = a.isPinned || pinnedProjectIdsSet.has(a.id)
+      const bIsPinned = b.isPinned || pinnedProjectIdsSet.has(b.id)
backend/migrations/clickhouse/2025_12_15_revenue.js (2)

30-30: Consider handling the migration result.

queriesRunner returns a boolean indicating success/failure, but the result is not being checked. For consistency with other migrations, consider awaiting and logging the result:

🔎 Suggested improvement
-queriesRunner(queries)
+const run = async () => {
+  const success = await queriesRunner(queries)
+  if (!success) {
+    process.exit(1)
+  }
+}
+
+run()

3-28: Schema duplication with initialise_database.js.

This table schema is duplicated in backend/migrations/clickhouse/initialise_database.js (lines 209-233). While this is common for migrations, be mindful that future schema changes need to be applied in both locations to maintain consistency for new deployments.

.claude/frontend-design.md (1)

1-45: LGTM - Good addition of design guidelines.

This document provides valuable guidance for maintaining consistent, high-quality frontend aesthetics. The guidelines around typography, color, motion, and avoiding generic patterns are well-articulated.

One minor note: Line 45 uses "thinking outside the box" which could be rephrased to align with the document's own advice about avoiding clichéd expressions.

web/app/pages/Project/Traffic/View/TrafficView.tsx (1)

182-182: Unused state variable revenueChartData.

The state revenueChartData is declared and set on line 641, but it's never read after that. The revenue data is instead embedded directly into chartWithRevenue and passed to setChartData. Consider removing this unused state to reduce complexity.

Suggested fix
-  const [revenueChartData, setRevenueChartData] = useState<number[]>([])

And remove line 641:

-          setRevenueChartData(revenueData)
backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1)

12-42: Consider adding validation for period and timezone fields.

The period field accepts any string, but it likely has a finite set of valid values (e.g., 7d, 30d, custom). Similarly, timezone should be a valid IANA timezone. Consider adding validation to catch invalid inputs early.

Optional: Add stricter validation
import { IsIn, IsTimeZone } from 'class-validator'

// For period (if you have a known set of values):
@IsIn(['7d', '30d', '90d', 'custom', 'all'])
period: string

// For timezone (class-validator has built-in timezone validation):
@IsOptional()
@IsTimeZone()
timezone?: string
backend/apps/cloud/src/analytics/analytics.module.ts (1)

12-12: Consider refactoring to avoid circular dependency.

AnalyticsModule and RevenueModule have a bidirectional circular dependency (both use forwardRef to import each other). While forwardRef resolves this at runtime, circular dependencies can indicate architectural coupling issues and make the codebase harder to maintain and test.

Consider:

  • Extracting shared interfaces/DTOs into a separate common module
  • Restructuring so only one module depends on the other (e.g., AnalyticsModule → RevenueModule, but not the reverse)
  • Using events or a mediator pattern to decouple the modules

If the circular dependency is intentional and necessary for the revenue analytics integration, add a comment explaining why both modules need to reference each other.

Also applies to: 20-20

web/app/api/index.ts (1)

2470-2494: Consider whether USD default is the appropriate choice for currency initialization.

The currency parameter defaults to 'USD' in the frontend function and backend endpoint (via the optional DTO field). While the UI allows users to explicitly select a currency, consider:

  • Whether this default should be inferred from project.revenueCurrency when already set
  • Whether requiring explicit currency selection (removing the default) would improve clarity
  • Documenting that USD is the fallback assumption when no currency is specified

The project entity already supports per-project currency tracking, so the infrastructure exists to make this decision automatic rather than defaulting to USD globally.

web/app/styles/ProjectViewStyle.css (1)

113-122: Consider using CSS variables for color consistency.

The revenue chart styling uses a hardcoded hex color #ea580c while most other target selectors (e.g., .bb-target-unique, .bb-target-total) use CSS variables like var(--color-amber-600). For maintainability and theme consistency, consider using a CSS variable.

Additionally, the revenue and refunds selectors are missing the !important flag on the stroke property that other targets have. Verify this is intentional based on the cascade behavior you need.

🔎 Suggested change for consistency
 /* Revenue chart styling */
 .bb-target-revenue .bb-bar {
-  stroke: #ea580c;
+  stroke: var(--color-orange-600) !important;
   fill-opacity: 0.85 !important;
 }

 .bb-target-refundsAmount .bb-bar {
-  stroke: #ea580c;
+  stroke: var(--color-orange-600) !important;
   fill-opacity: 1 !important; /* keep rgba() fill visible (don't multiply by .bb-bar opacity) */
 }
backend/migrations/mysql/2025_12_15_revenue.sql (1)

1-5: Consider combining ALTER TABLE statements for better performance.

On large tables, multiple ALTER TABLE statements can be slow as MySQL may rebuild the table for each statement. Consider combining them into a single statement for better migration performance.

🔎 Suggested optimization
 -- Revenue / Payment provider integration fields for Project table
-ALTER TABLE `project` ADD COLUMN `paddleApiKeyEnc` text DEFAULT NULL;
-ALTER TABLE `project` ADD COLUMN `revenueCurrency` varchar(3) DEFAULT NULL;
-ALTER TABLE `project` ADD COLUMN `paddleApiKeyPermissions` text DEFAULT NULL;
-ALTER TABLE `project` ADD COLUMN `revenueLastSyncAt` datetime DEFAULT NULL;
+ALTER TABLE `project`
+  ADD COLUMN `paddleApiKeyEnc` text DEFAULT NULL,
+  ADD COLUMN `revenueCurrency` varchar(3) DEFAULT NULL,
+  ADD COLUMN `paddleApiKeyPermissions` text DEFAULT NULL,
+  ADD COLUMN `revenueLastSyncAt` datetime DEFAULT NULL;

Additionally, if queries will filter or sort by revenueLastSyncAt (e.g., finding projects due for sync), consider adding an index:

ALTER TABLE `project` ADD INDEX `idx_project_revenue_last_sync` (`revenueLastSyncAt`);
web/app/pages/Project/View/components/UserDetails.tsx (1)

284-294: Minor inconsistency in currency source.

The revenue StatItem uses details.revenueCurrency for currency formatting, while the Sessions component receives currency from the parent prop. This creates two different currency sources within the same component.

If both should use the same currency (from the project), consider using the currency prop consistently:

🔎 Suggested change
               {details.totalRevenue !== undefined && details.totalRevenue > 0 ? (
                 <StatItem
                   icon={<DollarSignIcon className='h-3.5 w-3.5 text-green-500' />}
                   label={t('dashboard.revenue')}
                   value={new Intl.NumberFormat('en-US', {
                     style: 'currency',
-                    currency: details.revenueCurrency || 'USD',
+                    currency: currency || details.revenueCurrency || 'USD',
                     minimumFractionDigits: 2,
                   }).format(details.totalRevenue)}
                 />
               ) : null}

Alternatively, if details.revenueCurrency is the correct source for profile-level data while currency is for session-level display, document this distinction.

web/app/pages/Project/Settings/tabs/Revenue.tsx (1)

131-135: Consider improving error message extraction.

The error check typeof error === 'string' may not capture API error messages if they come as objects (e.g., { message: '...' }). This pattern is repeated in handleDisconnect and handleCurrencyChange as well.

🔎 Suggested helper for consistent error handling
// Could extract to a utility function
const getErrorMessage = (error: unknown, fallback: string): string => {
  if (typeof error === 'string') return error
  if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
    return error.message
  }
  return fallback
}

// Usage:
toast.error(getErrorMessage(error, t('apiNotifications.somethingWentWrong')))
backend/apps/cloud/src/revenue/dto/payment-log.dto.ts (1)

1-52: DTOs are solid; consider tightening currency validation

DTO shapes and basic validation look good for the new log endpoints. To avoid bad currency codes slipping through, you could strengthen the currency field validation:

  • Enforce 3‑letter codes.
  • Enforce uppercase.

Example:

Suggested currency validation refinement
-import { IsNotEmpty, IsString, IsOptional, IsNumber } from 'class-validator'
+import { IsNotEmpty, IsString, IsOptional, IsNumber, Length, IsUppercase } from 'class-validator'

  @ApiPropertyOptional({ description: 'Currency code' })
  @IsOptional()
-  @IsString()
+  @IsString()
+  @Length(3, 3)
+  @IsUppercase()
  currency?: string

This keeps the endpoint flexible while ensuring inputs match expected ISO-style codes.

backend/apps/cloud/src/task-manager/task-manager.service.ts (1)

62-63: Revenue sync cron is well-integrated; consider throttling and idempotency assumptions

The new syncRevenueData job is wired correctly into DI and filters projects sensibly (non-null paddleApiKeyEnc, paid/non-blocked admin). Error handling and per‑project logging look good.

Two non-blocking points to consider:

  1. Concurrency vs Paddle rate limits

    Promise.allSettled(_map(projects, ...)) will hit Paddle with one concurrent sync per project. If you grow to many Paddle-connected projects, this could stress your rate limits. You already have a mapLimit helper above; reusing it here would cap concurrency, e.g.:

    Example using existing mapLimit helper
  • const promises = _map(projects, async project => {
  •  try {
    
  •    const apiKey = this.revenueService.getPaddleApiKey(project)
    
  •    ...
    
  •  } catch (error) {
    
  •    this.logger.error(
    
  •      `[CRON WORKER](syncRevenueData) Error syncing project ${project.id}: ${error}`,
    
  •    )
    
  •  }
    
  • })
  • await Promise.allSettled(promises).catch(reason => {
  • await mapLimit(projects, 5, async project => {
  •  try {
    
  •    const apiKey = this.revenueService.getPaddleApiKey(project)
    
  •    ...
    
  •  } catch (error) {
    
  •    this.logger.error(
    
  •      `[CRON WORKER](syncRevenueData) Error syncing project ${project.id}: ${error}`,
    
  •    )
    
  •  }
    
  • }).catch(reason => {
    this.logger.error(
    [CRON WORKER](syncRevenueData) Error occured: ${reason},
    )
    })
</details>

2. **Multi-instance cron execution**

If the cloud app runs multiple instances, each will execute this cron. Make sure `paddleAdapter.syncTransactions` is idempotent (e.g. de-duping on `transactionId` at DB level) so double-runs don’t create duplicate records or inconsistent aggregates.





Also applies to: 328-329, 1083-1132

</blockquote></details>
<details>
<summary>web/app/pages/Project/View/ViewProject.helpers.tsx (1)</summary><blockquote>

`21-21`: **Revenue/refunds integration is well-wired; watch axis units and hard-coded “$”**

The revenue series wiring (columns, types, stacking with `refundsAmount`, colors, legend behaviour, and `onrendered` styling) is coherent and matches the intended “net revenue + dashed refunds overlay” design.

A few non-blocking concerns:

1. **Hard-coded USD in axis and tooltip**

- `y2.tick.format` and the new tooltip branches format values as `$${nFormatter(...)}`.
- Projects can configure a reporting currency; sessions UI already respects `revenueCurrency`, but this chart will always show `$`.

It would be more accurate to thread a `currencySymbol` into `getSettings` and use it instead of a literal `$`.

2. **Mixed units on `y2` axis**

`y2` now potentially carries **bounce (%)**, **session duration (time)**, and **revenue (currency)**:

```ts
axes: {
  bounce: 'y2',
  sessionDuration: 'y2',
  revenue: 'y2',
  refundsAmount: 'y2',
}
...
show: activeChartMetrics.bounce || activeChartMetrics.sessionDuration || activeChartMetrics.revenue
tick.format: activeChartMetrics.bounce
  ? d => `${d}%`
  : activeChartMetrics.revenue
    ? d => `$${nFormatter(d, 1)}`
    : d => getStringFromTime(...)

If users enable bounce + revenue (or sessionDuration + revenue) together, the axis unit and min/max (10–100 for bounce) no longer match all attached series and can clip revenue. Either gating these metrics to be mutually exclusive or separating revenue onto its own axis would avoid confusing scales.

  1. Reliance on billboard.js internals

    The onrendered hook manipulates chart.$.bar.bars directly:

    // @ts-expect-error
    const chart = this as any
    if (chart?.$ && chart.$.bar?.bars) {
      chart.$.bar.bars.each(function (this: SVGPathElement, d: any) { ... })
    }

    That works today but depends on private/internal fields that may change between billboard.js versions. It’s worth double-checking against your pinned billboard version and re-verifying on upgrades.

Also applies to: 236-236, 329-339, 633-667, 708-720, 755-778, 889-899, 900-939

web/app/pages/Project/View/components/Sessions.tsx (1)

29-40: Session-level revenue display is clear; improve currency fallback

The revenue display per session is nicely integrated (sign-aware, colored, and used in both compact and expanded layouts), and threading currency through SessionsSession keeps the API tidy.

One thing to adjust:

  • getCurrencySymbol currently falls back to USD even for unknown codes:

    const getCurrencySymbol = (currency?: string) => {
      return CURRENCY_SYMBOLS[currency || 'USD'] || CURRENCY_SYMBOLS.USD
    }

    If the backend ever sends, say, JPY or CHF, this will silently render them as $…. A safer approach is to:

    • Use USD only when currency is omitted, and
    • For unknown but present codes, fall back to the code itself (or no symbol).

    For example:

    Suggested safer currency fallback
    const getCurrencySymbol = (currency?: string) => {
  • return CURRENCY_SYMBOLS[currency || 'USD'] || CURRENCY_SYMBOLS.USD

  • if (!currency) {
  • return CURRENCY_SYMBOLS.USD
  • }
  • return CURRENCY_SYMBOLS[currency] ?? ${currency}
    }

</details>

This avoids mislabeling while still giving a sensible default when no currency is configured.



Also applies to: 65-66, 73-74, 82-83, 211-223, 243-255, 279-285, 303-304

</blockquote></details>
<details>
<summary>backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (2)</summary><blockquote>

`1-6`: **Consider removing unused enum values or marking them as planned.**

`POLAR` and `LEMONSQUEEZY` are defined in `RevenueProvider` but don't appear to be used anywhere in this PR. If these are planned for future implementation, consider adding a comment. Otherwise, remove them to avoid confusion.

---

`75-76`: **Production URL change required before merge.**

The TODO indicates this sandbox URL must be changed to `vendors.paddle.com` for production. This should be tracked to ensure it's not forgotten.


Would you like me to open an issue to track this production URL change?

</blockquote></details>
<details>
<summary>backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts (3)</summary><blockquote>

`66-79`: **Add timeout to prevent indefinite hangs on API calls.**

The `fetch` call has no timeout configured. If Stripe's API becomes unresponsive, this could block indefinitely.

<details>
<summary>🔎 Proposed fix using AbortController</summary>

```diff
async validateApiKey(apiKey: string): Promise<boolean> {
  try {
+     const controller = new AbortController()
+     const timeoutId = setTimeout(() => controller.abort(), 10000)
+
    const res = await fetch(`${this.baseUrl}/account`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
+       signal: controller.signal,
    })
+     clearTimeout(timeoutId)

    return res.ok
  } catch (error) {
    this.logger.error({ error }, 'Failed to validate Stripe API key')
    return false
  }
}

172-176: Apply same timeout pattern to paginated fetch calls.

The fetchAll method makes multiple API calls without timeouts. Each call should have a timeout to prevent sync jobs from hanging.


115-122: Sequential transaction processing may be slow for large datasets.

Processing transactions one-by-one with await in a loop can be slow when syncing many transactions. Consider batching inserts or using Promise.all with concurrency limits for better performance.

Also applies to: 131-139

backend/apps/cloud/src/analytics/analytics.controller.ts (2)

2384-2384: Avoid as any cast - use proper typing.

The cast { pid: dto.pid } as any bypasses type safety. Create a minimal interface or use a partial type instead.

🔎 Proposed fix
-      await this.analyticsService.validate({ pid: dto.pid } as any, origin, ip)
+      await this.analyticsService.validate({ pid: dto.pid } as Pick<PageviewsDto, 'pid'>, origin, ip)

Or if validate accepts a broader type, update its signature to reflect that.


2308-2332: Profile/session ID endpoints look correct but should also have rate limiting.

These public endpoints generate identifiers based on IP/UA fingerprinting. While less critical than payment logging, rate limiting would still be prudent to prevent enumeration attacks.

Also applies to: 2334-2369

backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (1)

87-101: Add timeouts to all fetch calls.

Same concern as Stripe adapter - all fetch calls should have timeouts to prevent indefinite hangs during sync operations.

Also applies to: 181-189, 239-247

web/app/pages/Project/View/components/Pageflow.tsx (1)

208-211: Use user's locale for currency formatting instead of hardcoded 'en-US'.

The component already has access to language from useTranslation() but uses hardcoded 'en-US' for currency formatting. This could display currencies incorrectly for non-US users (e.g., $1,234.56 vs 1.234,56 $).

🔎 Proposed fix

Since PageflowItem doesn't have direct access to language, pass it as a prop or use a more appropriate locale:

+  locale?: string
 }
 
 const PageflowItem = ({
   ...
+  locale = 'en-US',
 }: PageflowItemProps) => {
   ...
                       {new Intl.NumberFormat('en-US', {
+                      {new Intl.NumberFormat(locale, {
                         style: 'currency',
                         currency: currency || 'USD',
                       }).format(Math.abs(amount || 0))}

And pass from parent:

             <PageflowItem
               ...
+              locale={language}
             />
backend/apps/cloud/src/revenue/revenue.controller.ts (4)

165-193: Missing @apiresponse decorator for updateCurrency endpoint.

Other endpoints in this controller have @ApiResponse decorators, but updateCurrency is missing one for consistency with the API documentation.

🔎 Proposed fix
   @ApiBearerAuth()
   @Post('/:pid/revenue/currency')
   @Auth()
+  @ApiResponse({ status: 200 })
   async updateCurrency(

195-257: Missing @apiresponse decorator for syncRevenue endpoint.

For API documentation consistency, add the @ApiResponse decorator.

🔎 Proposed fix
   @ApiBearerAuth()
   @Post('/:pid/revenue/sync')
   @Auth()
+  @ApiResponse({ status: 200 })
   async syncRevenue(

291-295: Consider checking revenue connection status in analytics controller.

The getRevenueStats endpoint correctly checks for revenue configuration, but this pattern should be consistent across all analytics endpoints. The getRevenueTransactions and getRevenueBreakdown endpoints don't have this check, which could lead to confusing empty results instead of a clear error message.

🔎 Proposed fix for getRevenueTransactions
     this.projectService.allowedToView(project, userId)

+    if (!project.paddleApiKeyEnc && !project.stripeApiKeyEnc) {
+      throw new BadRequestException(
+        'Revenue tracking is not configured for this project',
+      )
+    }
+
     const safeTimezone = this.analyticsService.getSafeTimezone(dto.timezone)
🔎 Proposed fix for getRevenueBreakdown
     this.projectService.allowedToView(project, userId)

+    if (!project.paddleApiKeyEnc && !project.stripeApiKeyEnc) {
+      throw new BadRequestException(
+        'Revenue tracking is not configured for this project',
+      )
+    }
+
     const safeTimezone = this.analyticsService.getSafeTimezone(dto.timezone)

305-315: Type assertion as any for timeBucket parameter.

Multiple places cast timeBucket to any when passing to getGroupFromTo and generateXAxis. Consider properly typing the timeBucket variable or updating the DTO to use TimeBucketType.

backend/apps/cloud/src/revenue/revenue.service.ts (2)

52-63: Add error handling for malformed encrypted data in decrypt method.

The decrypt method will throw an unclear error if the encrypted text doesn't contain the expected : separator or has invalid hex encoding. Consider adding validation.

🔎 Proposed fix
   private decrypt(encryptedText: string): string {
+    if (!encryptedText || !encryptedText.includes(':')) {
+      throw new Error('Invalid encrypted data format')
+    }
     const [ivHex, encrypted] = encryptedText.split(':')
+    if (!ivHex || !encrypted || ivHex.length !== 32) {
+      throw new Error('Invalid encrypted data format')
+    }
     const iv = Buffer.from(ivHex, 'hex')
     const decipher = crypto.createDecipheriv(
       'aes-256-cbc',
       this.encryptionKey,
       iv,
     )
     let decrypted = decipher.update(encrypted, 'hex', 'utf8')
     decrypted += decipher.final('utf8')
     return decrypted
   }

629-701: Code duplication: getGroupSubquery and generateDateString are duplicated from AnalyticsService.

These methods are nearly identical to the ones in AnalyticsService. Consider extracting them to a shared utility module to maintain DRY principles.

#!/bin/bash
# Verify the duplication in AnalyticsService
rg -n "getGroupSubquery|generateDateString" backend/apps/cloud/src/analytics/analytics.service.ts
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4ac2b1 and bf62065.

📒 Files selected for processing (45)
  • .claude/frontend-design.md (1 hunks)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (3 hunks)
  • backend/apps/cloud/src/analytics/analytics.module.ts (1 hunks)
  • backend/apps/cloud/src/analytics/analytics.service.ts (14 hunks)
  • backend/apps/cloud/src/analytics/interfaces/index.ts (1 hunks)
  • backend/apps/cloud/src/app.module.ts (2 hunks)
  • backend/apps/cloud/src/project/entity/project.entity.ts (1 hunks)
  • backend/apps/cloud/src/project/project.service.ts (3 hunks)
  • backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (1 hunks)
  • backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/connect-paddle.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/payment-log.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/revenue-stats.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.controller.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.module.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.service.ts (1 hunks)
  • backend/apps/cloud/src/task-manager/task-manager.module.ts (2 hunks)
  • backend/apps/cloud/src/task-manager/task-manager.service.ts (3 hunks)
  • backend/apps/community/src/common/utils.ts (2 hunks)
  • backend/apps/community/src/project/project.controller.ts (2 hunks)
  • backend/migrations/clickhouse/2025_12_15_revenue.js (1 hunks)
  • backend/migrations/clickhouse/initialise_database.js (1 hunks)
  • backend/migrations/mysql/2025_12_15_revenue.sql (1 hunks)
  • backend/migrations/mysql/2025_12_18_revenue_stripe.sql (1 hunks)
  • web/app/api/index.ts (1 hunks)
  • web/app/lib/models/Project.ts (2 hunks)
  • web/app/pages/Project/Profiles/View/ProfilesView.tsx (1 hunks)
  • web/app/pages/Project/Sessions/View/SessionsView.tsx (1 hunks)
  • web/app/pages/Project/Settings/ProjectSettings.tsx (5 hunks)
  • web/app/pages/Project/Settings/tabs/DangerZone.tsx (3 hunks)
  • web/app/pages/Project/Settings/tabs/Revenue.tsx (1 hunks)
  • web/app/pages/Project/Traffic/View/TrafficView.tsx (7 hunks)
  • web/app/pages/Project/View/Panels.tsx (4 hunks)
  • web/app/pages/Project/View/ViewProject.helpers.tsx (8 hunks)
  • web/app/pages/Project/View/ViewProject.tsx (1 hunks)
  • web/app/pages/Project/View/components/Pageflow.tsx (9 hunks)
  • web/app/pages/Project/View/components/Sessions.tsx (7 hunks)
  • web/app/pages/Project/View/components/UserDetails.tsx (5 hunks)
  • web/app/styles/ProjectViewStyle.css (2 hunks)
  • web/app/ui/icons/Paddle.tsx (1 hunks)
  • web/app/ui/icons/Stripe.tsx (1 hunks)
  • web/public/locales/en.json (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
backend/migrations/clickhouse/2025_12_15_revenue.js (2)
backend/migrations/clickhouse/initialise_database.js (1)
  • require (2-2)
backend/migrations/clickhouse/setup.js (2)
  • dbName (107-107)
  • queriesRunner (76-105)
web/app/pages/Project/Sessions/View/SessionsView.tsx (1)
web/app/pages/Project/View/components/Sessions.tsx (1)
  • Sessions (279-310)
backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts (2)
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1)
  • RevenueTransaction (20-38)
web/app/api/index.ts (1)
  • RevenueProvider (2470-2470)
backend/apps/community/src/project/project.controller.ts (1)
backend/apps/community/src/common/utils.ts (1)
  • getPinnedProjectsClickhouse (1272-1272)
web/app/pages/Project/View/components/Sessions.tsx (1)
web/app/lib/models/Project.ts (1)
  • Session (95-115)
backend/apps/cloud/src/revenue/revenue.module.ts (2)
backend/apps/cloud/src/analytics/analytics.module.ts (1)
  • Module (14-26)
backend/apps/cloud/src/task-manager/task-manager.module.ts (1)
  • Module (20-40)
backend/apps/cloud/src/analytics/analytics.controller.ts (3)
backend/apps/cloud/src/revenue/dto/payment-log.dto.ts (3)
  • GetProfileIdDto (4-9)
  • GetSessionIdDto (11-16)
  • LogPaymentDto (18-52)
backend/apps/cloud/src/common/utils.ts (1)
  • getIPFromHeaders (285-312)
web/app/api/index.ts (2)
  • RevenueProvider (2470-2470)
  • RevenueStatus (2436-2441)
web/app/pages/Project/View/ViewProject.helpers.tsx (2)
web/app/utils/generic.ts (3)
  • nFormatter (20-24)
  • getStringFromTime (81-91)
  • getTimeFromSeconds (64-79)
web/fly_import_env.cjs (1)
  • value (14-14)
web/app/pages/Project/View/components/UserDetails.tsx (1)
web/app/pages/Project/View/components/Sessions.tsx (1)
  • Sessions (279-310)
backend/apps/cloud/src/revenue/revenue.service.ts (3)
backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts (1)
  • Injectable (57-346)
backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (1)
  • Injectable (77-387)
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (2)
  • PADDLE_REQUIRED_PERMISSIONS (67-73)
  • RevenueTransaction (20-38)
backend/apps/cloud/src/analytics/analytics.module.ts (3)
backend/apps/cloud/src/revenue/revenue.module.ts (1)
  • Module (16-27)
backend/apps/cloud/src/app.module.ts (1)
  • Module (97-111)
backend/apps/cloud/src/task-manager/task-manager.module.ts (1)
  • Module (20-40)
backend/migrations/clickhouse/initialise_database.js (1)
backend/migrations/clickhouse/setup.js (1)
  • dbName (107-107)
web/app/pages/Project/Settings/ProjectSettings.tsx (1)
web/app/lib/constants/index.ts (1)
  • isSelfhosted (319-319)
🪛 LanguageTool
.claude/frontend-design.md

[style] ~45-~45: “thinking outside the box” can be a clichéd phrase in professional communication. Consider an alternative expression to make your writing more engaging and original.
Context: ...ck, show what can truly be created when thinking outside the box and committing fully to a distinctive v...

(THINK_OUTSIDE_THE_BOX)

🔇 Additional comments (50)
backend/apps/community/src/project/project.controller.ts (2)

294-305: LGTM!

The isPinned flag is correctly computed for each project in the response payload, combining both the project's own flag and the pinned IDs lookup.


307-313: LGTM!

The return structure correctly uses the pre-sorted results. The comment accurately documents that pinned sorting was already applied in the combined sort step above.

web/app/pages/Project/View/Panels.tsx (1)

716-716: Transparent sticky header may cause readability issues when scrolling.

Changing the sticky header background from bg-white dark:bg-slate-800/25 to bg-transparent will allow scrolling content to show through the header, potentially making the column labels difficult to read. The same change applies to lines 1120, 1201, and 1733.

If this is intentional (e.g., parent container now provides the background), please disregard. Otherwise, consider retaining an opaque background for sticky headers.

web/app/ui/icons/Paddle.tsx (1)

1-29: LGTM!

Good use of useId() to generate unique clipPath IDs, which prevents ID collisions when multiple instances of this component are rendered on the same page.

backend/migrations/clickhouse/initialise_database.js (2)

224-224: Verify PII storage compliance for customer_email.

Storing customer_email in the analytics database may have GDPR/CCPA implications. Ensure this aligns with your data retention policies and privacy documentation, and consider whether hashing or pseudonymization would be more appropriate for analytics purposes.


209-233: Revenue table schema looks well-designed.

The schema appropriately uses:

  • ReplacingMergeTree(synced_at) for idempotent transaction updates
  • Decimal64(4) for monetary amounts (4 decimal places)
  • LowCardinality for low-entropy string columns (provider, type, status, currencies)
  • Appropriate compression codecs
web/app/pages/Project/Traffic/View/TrafficView.tsx (2)

261-316: Well-structured chart metrics with proper conflict handling.

The revenue metric is correctly gated for non-selfhosted deployments and properly defines conflicts with bounce rate and session duration metrics. The .filter(Boolean) pattern cleanly handles the conditional inclusion.


764-780: Correct dependency addition for revenue metric toggle.

Adding activeChartMetrics.revenue to the dependency array ensures analytics reload when the revenue metric is toggled, which is the expected behavior.

backend/apps/cloud/src/revenue/dto/revenue-stats.dto.ts (1)

3-27: DTOs are well-structured with appropriate Swagger documentation.

The remaining DTOs (RevenueStatsDto, RevenueChartDto, RevenueBreakdownItemDto, RevenueBreakdownDto, RevenueStatusDto) are properly defined with clear property descriptions and correct typing. RevenueStatusDto correctly uses required: false for its optional fields.

Also applies to: 29-41, 84-113, 115-129

backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1)

44-71: Pagination and filtering DTOs are well-implemented.

GetRevenueTransactionsDto correctly extends the base DTO and adds pagination with sensible defaults and bounds (take: 1-100, skip: ≥0). The @Type(() => Number) decorator ensures query string parameters are properly converted.

backend/apps/community/src/common/utils.ts (2)

302-316: Clean implementation of pinned-first ordering.

The two-stage ordering logic correctly prioritizes pinned projects first (isPinned DESC) while maintaining the user's selected sort preference as a secondary criterion. The approach is consistent and well-structured.


318-363: Query implementation is secure and consistent.

Both search and non-search query paths:

  • Use parameterized queries preventing SQL injection
  • Correctly apply the LEFT JOIN for pinned status
  • Maintain consistent isPinned computation via CASE expression

The ILIKE operator provides case-insensitive search which is appropriate for project name/ID lookups.

web/app/pages/Project/View/ViewProject.tsx (1)

78-78: LGTM - Helpful architectural documentation.

The comment clearly explains that revenue data is now integrated into TrafficView, Pageflow, and Profile details instead of having a dedicated dashboard. This helps future developers understand the design decision.

web/app/api/index.ts (3)

2435-2468: LGTM - Well-structured Revenue API types.

The Revenue API type definitions are clear and comprehensive. The interfaces for RevenueStatus, RevenueStats, and RevenueChart provide good type safety for the frontend.


2495-2524: LGTM - Clean API implementation with backward compatibility.

The revenue management endpoints follow the existing patterns well:

  • Consistent error handling
  • Proper TypeScript return types
  • Backward compatibility aliases (connectPaddleRevenue, disconnectPaddleRevenue) ensure existing code won't break

2525-2541: LGTM - Flexible revenue data retrieval.

The getRevenueData endpoint properly supports optional parameters for period, date ranges, timezone, and time bucket, making it flexible for various frontend use cases.

backend/apps/cloud/src/app.module.ts (1)

36-36: LGTM - Clean module integration.

RevenueModule is properly imported and registered in the top-level AppModule, making revenue functionality available throughout the cloud application.

Also applies to: 94-94

web/app/lib/models/Project.ts (2)

103-104: LGTM - Clean revenue tracking for sessions.

Adding optional revenue and refunds fields to the Session interface enables per-session revenue tracking. The optional markers are appropriate since not all sessions will have revenue data.


169-170: LGTM - Profile revenue aggregation support.

Adding totalRevenue and revenueCurrency to ProfileDetails enables displaying total revenue per user profile. Using optional fields is correct since:

  • Not all projects will have revenue integration enabled
  • Historical profiles won't have revenue data
  • Currency information depends on revenue provider configuration
backend/apps/cloud/src/task-manager/task-manager.module.ts (1)

18-18: RevenueModule import is necessary and properly used.

TaskManagerService directly depends on RevenueService and PaddleAdapter from RevenueModule (injected in constructor at lines 328-329). The circular dependency chain (TaskManager → Revenue → Analytics) is already handled with forwardRef on both sides of the Revenue-Analytics relationship, making this import safe.

web/app/pages/Project/Sessions/View/SessionsView.tsx (1)

304-304: LGTM!

The currency prop is correctly passed to the Sessions component using optional chaining on project?.revenueCurrency, which safely handles cases where the project may be undefined. This aligns with the revenue currency propagation pattern seen in other components like ProfilesView.tsx and UserDetails.tsx.

web/app/styles/ProjectViewStyle.css (1)

78-81: LGTM!

Removing !important from the base .bb-bar stroke-width rule allows the more specific revenue chart rules to properly cascade and override when needed. This is the correct approach for managing CSS specificity.

web/app/pages/Project/Profiles/View/ProfilesView.tsx (1)

340-340: LGTM!

The currency prop is correctly passed to UserDetails with safe optional chaining. This aligns with the revenue currency propagation pattern established across the frontend.

web/app/pages/Project/Settings/ProjectSettings.tsx (2)

353-358: LGTM!

The revenue tab is properly configured:

  • Correctly gated with visible: !isSelfhosted to restrict to cloud edition
  • Follows the established tab configuration pattern
  • Icon and translation key are appropriately set

1105-1106: LGTM!

The conditional rendering for the Revenue component follows the same pattern as other tabs and correctly passes the projectId prop.

web/app/pages/Project/View/components/UserDetails.tsx (1)

396-402: LGTM!

The currency prop is correctly forwarded to the Sessions component, maintaining currency context for session-level revenue display.

backend/apps/cloud/src/project/project.service.ts (3)

452-466: LGTM!

The database-level pinning implementation is well-designed:

  • The CASE WHEN expression correctly computes isPinned as 1 or 0
  • The LEFT JOIN appropriately handles projects that aren't pinned (they'll have NULL and evaluate to 0)
  • The userId parameter is already bound via setParameter in the where clause and reused in the join condition

486-505: LGTM!

Good refactor moving pin sorting to the database level:

  • Primary sort by isPinned DESC ensures pinned projects always appear first
  • Using addOrderBy for secondary sorts correctly preserves the primary sort order
  • This is more efficient than the previous client-side reordering approach

1707-1708: LGTM!

The comment accurately documents that client-side reordering is no longer needed since pinning is handled at the query level. This improves maintainability by explaining the architectural decision.

backend/apps/cloud/src/revenue/revenue.module.ts (1)

16-27: LGTM!

The module is well-structured with appropriate use of forwardRef to handle circular dependencies between RevenueModule and AnalyticsModule. The provider exports allow other modules like TaskManagerModule to consume revenue functionality as needed.

backend/apps/cloud/src/revenue/dto/connect-paddle.dto.ts (1)

34-43: LGTM!

The DTO correctly validates ISO 4217 currency codes with the appropriate length constraint.

web/app/pages/Project/Settings/tabs/Revenue.tsx (1)

86-117: LGTM!

The component's state management and data loading logic is well-structured with appropriate use of useCallback for memoization and proper cleanup of loading states in the finally block.

backend/apps/cloud/src/project/entity/project.entity.ts (1)

174-192: LGTM! Entity fields are well-defined.

The revenue-related columns are appropriately typed: text for encrypted API keys (accommodating variable encrypted data length), varchar(3) for ISO 4217 currency codes, and datetime for the sync timestamp. All fields being nullable allows existing projects to remain unaffected.

Note the comment at line 30: "In case of modifying some properties here add them to the GDPR data export email template." Verify whether encrypted API keys or revenueLastSyncAt should be included in GDPR data exports. Encrypted keys are likely excluded intentionally, but the sync timestamp may be relevant.

backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts (1)

17-38: LGTM!

The apiKey and currency fields are properly validated. The API key format validation is appropriately deferred to provider-specific logic since Stripe and Paddle have different key formats.

web/public/locales/en.json (1)

890-891: Revenue-related copy and keys look consistent

The added sales/xSales, dashboard revenue/refunds/revenueHint, project settings revenue tab (including connection/help text), and the project-level revenue analytics block are all consistent with existing naming and tone, and the placeholders (e.g. {{count}}, {{date}}) match established patterns.

Also applies to: 914-916, 1497-1553, 1708-1729

backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1)

8-88: Well-structured domain model for revenue tracking.

The interfaces and enums provide a clean, type-safe foundation for the revenue tracking system. The separation of transaction data, stats, chart data, and breakdown is appropriate for the analytics use case.

backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts (1)

81-143: Good overlap window implementation to avoid missing edge-case transactions.

The 60-second overlap window is a solid approach to handle timing differences between systems and avoid missing transactions at sync boundaries.

backend/apps/cloud/src/analytics/analytics.controller.ts (1)

100-110: Clean integration of revenue service into analytics controller.

The imports and dependency injection are properly structured, following the existing patterns in this controller.

Also applies to: 218-218

backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (3)

79-80: Production URL change required before merge.

Same as the interface file - this sandbox URL must be changed to api.paddle.com for production.


103-151: Solid sync implementation with proper error propagation.

Good that errors are re-thrown after logging (line 147), allowing callers to handle failures appropriately. The overlap window logic mirrors the Stripe adapter correctly.


153-211: Well-implemented pagination with proper cursor handling.

The pagination logic correctly handles the has_more flag and next cursor from Paddle's API response. The early termination when has_more is false prevents unnecessary API calls.

Also applies to: 213-269

web/app/pages/Project/View/components/Pageflow.tsx (3)

154-160: Good color differentiation for transaction types.

The green/orange color scheme for sale/refund events provides clear visual distinction and follows common financial UI conventions (green = positive, orange = reversal/warning).


199-229: Clean implementation of conditional rendering for financial events.

The separation of sale/refund display logic from pageview/event/error using a conditional block keeps the code readable. The negative sign handling for refunds is correct.


27-52: Well-extended type definitions for revenue events.

The type extensions are clean and maintain backward compatibility with existing pageview/event/error types while adding the necessary fields for financial data.

backend/apps/cloud/src/analytics/analytics.service.ts (4)

4249-4278: LGTM - Clean extraction of revenue metadata for sale/refund events.

The logic correctly extracts amount and currency from metadata for sale/refund page types, filters them out of the returned metadata array, and parses the amount as a float. The fallback to undefined when entries are not found is appropriate.


4340-4362: LGTM - Revenue events integrated into session pageflow.

The UNION ALL clause correctly joins revenue data into the session events timeline with appropriate metadata fields (amount, currency, transaction_id, status, provider). The filtering by type IN ('sale', 'refund') ensures only relevant transaction types are included.


4589-4600: LGTM - Revenue totals CTE correctly aggregates session revenue.

The CTE properly calculates net revenue (sales minus refunds) and total refunds per session. Using abs(amount) for refunds handles negative amounts correctly.


4820-4830: Profile revenue query uses correct column name.

The query correctly filters by profile_id = {profileId:String}, which matches the column name used in the insertTransaction method in revenue.service.ts. No issues found.

backend/apps/cloud/src/revenue/revenue.service.ts (2)

587-621: LGTM - Transaction insertion with proper date formatting.

The insertTransaction method correctly formats dates for ClickHouse and handles optional fields with null coalescing. The metadata serialization using JSON.stringify is appropriate.


188-292: LGTM - Revenue stats query with period comparison.

The stats query correctly uses a subquery with argMax to get the latest state per transaction, preventing duplicate counting. The period comparison calculation is well-implemented.

@Blaumaus Blaumaus self-assigned this Dec 19, 2025
@Blaumaus
Copy link
Member Author

@CodeRabbit is it good now?

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine the encryption implementation in revenue.service.ts
cat backend/apps/cloud/src/revenue/revenue.service.ts | head -100

Length of output: 3053


🏁 Script executed:

#!/bin/bash
# Check the currency conversion logic in both adapters
echo "=== Paddle Adapter Currency Conversion ==="
cat backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts | grep -A 20 "convertCurrency"

echo -e "\n=== Stripe Adapter Currency Conversion ==="
cat backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts | grep -A 20 "convertCurrency"

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Check for hardcoded secrets or API keys
rg -i "api[_-]?key.*=.*['\"]" --type ts --glob '!*.test.ts' --glob '!*.spec.ts' -C 2

Length of output: 2105


🏁 Script executed:

#!/bin/bash
# Review the payment logging endpoint for security issues
cat backend/apps/cloud/src/analytics/analytics.controller.ts | grep -A 50 "logPayment"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are TODO comments or FIXMEs
rg -i "TODO|FIXME|XXX|HACK" --type ts --type tsx -g 'backend/apps/cloud/src/revenue/**' -g 'web/app/pages/Project/Settings/tabs/Revenue.tsx'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Examine the revenue migration scripts
echo "=== ClickHouse Migration ==="
cat backend/migrations/clickhouse/2025_12_15_revenue.js

echo -e "\n=== MySQL Migrations ==="
cat backend/migrations/mysql/2025_12_15_revenue.sql
cat backend/migrations/mysql/2025_12_18_revenue_stripe.sql

Length of output: 1675


🏁 Script executed:

#!/bin/bash
# Check for SQL injection vulnerabilities in revenue queries
rg "SELECT|INSERT|UPDATE|DELETE" backend/apps/cloud/src/revenue/revenue.service.ts -C 3

Length of output: 2886


🏁 Script executed:

#!/bin/bash
# Review the cron job implementation
cat backend/apps/cloud/src/task-manager/task-manager.service.ts | grep -A 30 "syncRevenueData"

Length of output: 2834


🏁 Script executed:

#!/bin/bash
# Find the convertCurrency method implementation
ast-grep --pattern 'convertCurrency($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the payment logging endpoints
cat backend/apps/cloud/src/analytics/analytics.controller.ts | grep -B 5 -A 60 "@Post('payment')"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the Paddle adapter for potential issues
cat backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts | head -200

Length of output: 4782


🏁 Script executed:

#!/bin/bash
# Check the Stripe adapter for potential issues
cat backend/apps/cloud/src/revenue/adapters/stripe.adapter.ts | head -200

Length of output: 5056


🏁 Script executed:

#!/bin/bash
# Check if public endpoints have rate limiting
rg "@Public\(\)" backend/apps/cloud/src/analytics/analytics.controller.ts -A 5

Length of output: 1178


🏁 Script executed:

#!/bin/bash
# Look for validation on the payment endpoints
cat backend/apps/cloud/src/revenue/dto/payment-log.dto.ts

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Check for sensitive data logging
rg "logger\.(log|error|warn|debug)" backend/apps/cloud/src/revenue/ -C 2 | head -100

Length of output: 7542


🏁 Script executed:

#!/bin/bash
# Check the revenue controller for authorization issues
cat backend/apps/cloud/src/revenue/revenue.controller.ts | head -150

Length of output: 4263


🏁 Script executed:

#!/bin/bash
# Find payment logging endpoints more broadly
rg "logPayment|profile-id|session-id|/payment" backend/apps/cloud/src/analytics/analytics.controller.ts -C 10

Length of output: 906


🏁 Script executed:

#!/bin/bash
# Check the payment log DTO file
fd "payment-log.dto" backend/apps/cloud/src/revenue/dto/ --exec cat {}

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the currency service implementation
cat backend/apps/cloud/src/revenue/currency.service.ts

Length of output: 2578


🏁 Script executed:

#!/bin/bash
# Check rate limiting configuration
rg "RateLimit|rate.?limit" backend/apps/cloud/src/ --type ts -C 3 | head -50

Length of output: 4299


🏁 Script executed:

#!/bin/bash
# Review the entire analytics controller for new endpoints
rg "@Post\('(profile-id|session-id|payment)'\)" backend/apps/cloud/src/analytics/ -B 2 -A 30

Length of output: 4772


🏁 Script executed:

#!/bin/bash
# Check if there are unit tests for revenue module
fd "revenue.*\.(test|spec)\.ts" backend/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for proper error handling in adapters
rg "throw new|catch" backend/apps/cloud/src/revenue/adapters/ -C 2

Length of output: 3003


🏁 Script executed:

#!/bin/bash
# Verify the SQL queries use parameterization
cat backend/apps/cloud/src/revenue/revenue.service.ts | grep -A 5 "clickhouse.query"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the actual payment logging endpoint implementation
rg "logPayment" backend/apps/cloud/src/analytics/analytics.controller.ts -B 3 -A 50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if DTOs exist in analytics folder
fd "payment-log.dto" backend/apps/cloud/src/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at ClickHouse query execution
rg "clickhouse\.(query|insert)" backend/apps/cloud/src/revenue/revenue.service.ts -C 3

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Check validation decorators on revenue DTOs
cat backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts

Length of output: 949


🏁 Script executed:

#!/bin/bash
# Check if there's input validation on amounts
rg "IsNumber|IsPositive|Min|Max" backend/apps/cloud/src/revenue/dto/ -C 2

Length of output: 1158


@Blaumaus: I've completed a comprehensive review of your Revenue Analytics PR. This is a substantial feature addition with solid architecture, but there are several important issues that need attention before merging:

🔴 Critical Security Issues

1. Missing Rate Limiting on Public Endpoints

The new profile-id and session-id endpoints in analytics.controller.ts are marked @Public() but lack rate limiting:

@Post('profile-id')
@Public()
async getOrCreateProfileId(...)

Recommendation: Add rate limiting similar to other public endpoints:

@Post('profile-id')
@Public()
async getOrCreateProfileId(
  @Body() dto: GetProfileIdDto,
  @Headers() headers,
  @Ip() reqIP,
): Promise<{ profileId: string | null }> {
  const ip = getIPFromHeaders(headers, true) || reqIP || ''
  await checkRateLimit(ip, 'revenue-attribution', 100, 1800)
  // ... rest of implementation
}

2. Encryption Implementation ✅

The encryption looks secure - using AES-256-CBC with proper IV generation and scrypt for key derivation. Good job on this!

⚠️ High Priority Issues

3. No Unit Tests

The entire revenue module lacks tests. Critical functionality like:

  • Currency conversion logic
  • Payment provider adapters
  • Encryption/decryption
  • Revenue calculations

Recommendation: Add tests at minimum for:

  • CurrencyService.convert() - currency conversion logic
  • RevenueService encryption/decryption methods
  • Adapter validation and sync methods
  • Revenue statistics calculations

4. Cron Job Frequency Concerns

@Cron(CronExpression.EVERY_30_MINUTES)
async syncRevenueData()

Running every 30 minutes might hit Paddle/Stripe API rate limits, especially as you scale to more projects.

Recommendation:

  • Start with hourly syncing
  • Implement exponential backoff on rate limit errors
  • Consider adaptive scheduling based on transaction volume

5. Currency Conversion Fallback Risk

In currency.service.ts, when rates are unavailable, it falls back to 1:1 conversion:

if (!rates) {
  this.logger.warn(
    { fromCurrency, toCurrency },
    'Currency rates unavailable, falling back to 1:1 conversion',
  )
  return amount
}

This could lead to significantly incorrect revenue reporting.

Recommendation:

  • Throw an error instead of silent fallback
  • Retry rate fetching with backoff
  • Consider caching last known good rates as backup
  • Alert when fallback occurs

6. Missing Rollback/Down Migrations

Your migration files only have up logic. If something goes wrong in production, you can't easily roll back.

Recommendation: Add down migrations, especially for:

  • 2025_12_15_revenue.js (ClickHouse)
  • 2025_12_15_revenue.sql and 2025_12_18_revenue_stripe.sql (MySQL)

7. Partial Sync Failure Handling

In the adapters, if syncing 50 transactions succeeds but transaction 51 fails, the first 50 are committed:

for (const transaction of transactions) {
  await this.processTransaction(projectId, transaction, currency)
  syncedCount++
}

Recommendation: Consider batching with transaction semantics or implement idempotency checks to allow safe retries.

🟡 Medium Priority Issues

8. ClickHouse Table Optimization

The revenue table in 2025_12_15_revenue.js could benefit from additional indexes for common queries:

-- Consider adding these projections for better query performance
ALTER TABLE revenue ADD PROJECTION revenue_by_date (
  SELECT * ORDER BY created, pid
);

9. Error Message Information Disclosure

Some error messages might leak implementation details:

throw new BadRequestException(
  'Invalid Paddle API key. Please check your key and try again.',
)

This confirms to attackers that Paddle is being used. Consider generic messages for production.

10. Adapter Code Duplication

PaddleAdapter and StripeAdapter have very similar structure. Consider extracting common logic to a base class:

abstract class PaymentProviderAdapter {
  protected abstract validateApiKey(apiKey: string): Promise<boolean>
  protected abstract syncTransactions(...): Promise<number>
  // Common logging, error handling, etc.
}

✅ Positive Aspects

  1. Clean Architecture - The adapter pattern is well-implemented
  2. Proper Encryption - API keys are encrypted with AES-256-CBC
  3. Input Validation - DTOs have comprehensive validation decorators
  4. SQL Injection Protection - Using parameterized queries
  5. Permission Checks - Proper authorization on revenue endpoints
  6. Comprehensive Logging - Good observability without leaking sensitive data
  7. Type Safety - Excellent TypeScript usage with interfaces and DTOs

📝 Minor Suggestions

  1. Frontend Design Document - The .claude/frontend-design.md file seems unrelated to this PR. Consider removing it or creating a separate PR.

  2. API Documentation - Consider adding examples to Swagger docs for the revenue endpoints showing the expected response formats.

  3. Monitoring - Add metrics for:

    • Sync success/failure rates
    • Currency conversion fallback occurrences
    • API key validation failures

Summary

This is a well-architected feature with solid foundations, but it needs attention to security and reliability before going to production:

Must Fix Before Merge:

  • ✅ Add rate limiting to public endpoints
  • ✅ Add unit tests (at least for critical paths)
  • ✅ Fix currency conversion fallback logic
  • ✅ Add down migrations

Should Fix Soon After:

  • Review cron job frequency
  • Implement transaction/batch semantics
  • Add monitoring and alerting
  • Reduce code duplication in adapters

Would you like me to help you address any of these specific issues?

@Blaumaus Blaumaus merged commit 885ff37 into swetrix-revamp Dec 19, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 20, 2025
9 tasks
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