Skip to content

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Dec 5, 2025

Related PRs:

Summary by CodeRabbit

  • New Features

    • Streaming AI chat with chat management, A/B experiments (Bayesian results), feature flags & evaluations, revenue integrations (Stripe & Paddle), goals/conversion tracking, profile/session analytics with realtime heartbeat, project pin/unpin, proof-of-work CAPTCHA, frontend design guidelines document.
  • Improvements

    • Expanded analytics endpoints and input validation, timezone-safe queries, OG-image rate limiting and safer decoding, stricter webhook validation, pinned-projects UX and persistence, safer IP/analytics tracking.

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

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: 1

🧹 Nitpick comments (3)
backend/apps/cloud/src/analytics/analytics.controller.ts (3)

1827-1832: Consider structured validation for options parameter

The JSON parsing with a catch-all fallback is safe but could benefit from explicit type validation using a DTO class with class-validator decorators, rather than parsing untyped JSON. This would provide better type safety and API documentation.

Example: Structured options DTO

Define a separate DTO class:

class ErrorOverviewOptionsDto {
  @IsOptional()
  @IsBoolean()
  showResolved?: boolean = false
}

Then parse and validate:

const parsedOptions = plainToInstance(ErrorOverviewOptionsDto, JSON.parse(options || '{}'))
await validate(parsedOptions)

1884-1941: Consider returning appliedFilters for consistency

Most list endpoints (e.g., getSessions line 1655, getProfiles line 2052) return appliedFilters in their response. The error-sessions endpoint should consider including this for UI consistency, even if no filters are currently applied to this query.


2323-2347: Clarify session-id endpoint behavior and consider rate limiting

The endpoint returns a deterministic session ID without creating a backing Redis session (it calls getSessionId but ignores the exists flag and doesn't call generateAndStoreSessionId).

Consider:

  1. Adding a code comment or API documentation clarifying this is a deterministic ID derivation helper that does not create session state
  2. Adding rate limiting, as this is a public endpoint similar to other data-generation endpoints
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59cae9c and 0739a8c.

📒 Files selected for processing (1)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (21 hunks)
🔇 Additional comments (13)
backend/apps/cloud/src/analytics/analytics.controller.ts (13)

111-111: LGTM: Clear constant for online visitor window

The 5-minute window for online visitors is well-defined and appropriately scoped for use in the live-visitors endpoint.


858-898: LGTM: includeChart parameter addition

The includeChart parameter allows clients to control whether chart data is included in the analytics summary response, providing a useful optimization for cases where only aggregate stats are needed.


1116-1127: LGTM: Profile tracking integrated into error logging

The error endpoint now properly generates and tracks profile IDs, recording session activity before logging the error event. The pattern is consistent and correct.


1255-1266: LGTM: Profile tracking integrated into custom event logging

Consistent implementation of profile ID generation and session activity recording for custom events.


1330-1350: LGTM: Heartbeat logic correctly handles existing sessions

The heartbeat endpoint properly validates that a session exists before extending its TTL and recording activity. The use of getSessionId instead of generateAndStoreSessionId is appropriate since heartbeats should only extend existing sessions.


1378-1389: LGTM: Profile tracking integrated into pageview logging

The main logging endpoint correctly generates profile IDs and records session activity for pageviews.


1524-1534: LGTM: Profile tracking for no-JavaScript fallback

The noscript endpoint appropriately generates server-side profile IDs since it can't accept client-provided IDs via query parameters.


1801-1882: LGTM: Error overview endpoint implementation

The endpoint correctly handles project access checks, time bucket calculations, and filter processing. The options parsing is defensive with appropriate fallbacks.


1971-2053: LGTM: Profiles list endpoint

The endpoint follows established patterns with appropriate pagination limits, access controls, and filter handling. The profileType parameter provides useful filtering capability.


2104-2125: LGTM: Efficient parallel data fetching for profile details

The endpoint efficiently fetches multiple profile data points in parallel using Promise.all, providing a comprehensive profile view with details, top pages, activity calendar, and chart data.


2127-2209: LGTM: Profile sessions list endpoint

The endpoint correctly implements session listing for a specific profile with standard pagination, filtering, and access control patterns.


2269-2280: LGTM: Defensive parsing of custom events parameter

The JSON parsing with try-catch and fallback to empty array is safe. The early return for empty lists avoids unnecessary database queries. The empty chart structure is consistent with the response format.


1046-1080: No issues found. The query uses valid ClickHouse syntax with DISTINCT ON (psid), which is officially supported in ClickHouse for deduplication.

Comment on lines +2297 to +2321
@Post('profile-id')
@Public()
async getOrCreateProfileId(
@Body() dto: GetProfileIdDto,
@Headers() headers,
@Ip() reqIP,
): Promise<{ profileId: string | null }> {
const { 'user-agent': userAgent } = headers
const { pid } = dto

try {
const ip = getIPFromHeaders(headers, true) || reqIP || ''

const profileId = await this.analyticsService.generateProfileId(
pid,
userAgent,
ip,
)

return { profileId }
} catch (error) {
this.logger.error({ error, pid }, 'Error generating profile ID')
return { profileId: null }
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add rate limiting to the public profile-id endpoint

This endpoint generates deterministic profile IDs based on request metadata. Without rate limiting, it's vulnerable to bulk enumeration attempts and cross-project correlation attacks. Consider applying @nestjs/throttler or express-rate-limit to restrict requests per IP/client.

🤖 Prompt for AI Agents
In backend/apps/cloud/src/analytics/analytics.controller.ts around lines 2297 to
2321, the public POST /profile-id endpoint is unthrottled and must be rate
limited to prevent bulk enumeration; add NestJS throttling by applying a
ThrottlerGuard and a per-route throttle (e.g., @UseGuards(ThrottlerGuard) and
@Throttle(x, y)) or wire a custom guard that keys limits by IP and user-agent,
update imports and module providers to include ThrottlerModule/ThrottlerGuard
(or integrate express-rate-limit middleware) and tune limits (requests/window)
appropriate for legitimate clients while ensuring the Public() decorator still
allows unauthenticated access but not bypassing throttling.

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: 2

🧹 Nitpick comments (1)
backend/apps/cloud/src/analytics/analytics.service.ts (1)

319-346: Origin wildcard validation is safe from ReDoS; focus on other potential improvements.

The isValidOrigin function's use of escapeRegex to escape all regex special characters prevents injection of malicious quantifiers. The resulting regex pattern /^.*\.[escaped].*$/ has a linear structure without nested quantifiers, which prevents catastrophic backtracking.

While this implementation is secure against ReDoS attacks, consider these optional improvements for robustness:

  1. Cache compiled regex patterns to avoid recompilation on each call
  2. Add a reasonable length limit on origin patterns
  3. Validate origin patterns at project creation time rather than on every request

The current implementation's security posture is sound; ReDoS is not a practical risk here.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0739a8c and 12c6f70.

📒 Files selected for processing (1)
  • backend/apps/cloud/src/analytics/analytics.service.ts (54 hunks)
🧰 Additional context used
🪛 ast-grep (0.40.0)
backend/apps/cloud/src/analytics/analytics.service.ts

[warning] 335-335: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${parts.join('.*')}$, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (15)
backend/apps/cloud/src/analytics/analytics.service.ts (15)

1133-1201: LGTM! Solid session management refactor.

The salt-based PSID derivation with SET NX semantics provides:

  • Race-condition-free session creation (line 1178-1183)
  • Deterministic session IDs within salt rotation periods
  • Atomic new-session detection

The implementation correctly handles:

  • Extending TTL for existing sessions (line 1192)
  • 8-byte hash extraction for reasonable collision resistance

Verify that salt rotation policies are documented and that transitioning between salts doesn't break active long-lived sessions unexpectedly.


1240-1286: LGTM! Session activity tracking correctly implemented.

The ClickHouse integration properly:

  • Uses FINAL to read deduplicated session data (line 1250)
  • Preserves firstSeen from existing sessions (line 1277)
  • Updates lastSeen on every activity (line 1278)
  • Uses async_insert for better write performance (line 1281)

Error handling is appropriate with logging but no throws, preventing pageview failures due to session tracking issues.

Verify the performance impact of this method being called on every pageview/event. Consider batching or sampling for high-traffic projects.


1288-1346: LGTM! Session persistence queries are well-implemented.

Both methods correctly:

  • Use FINAL for consistent reads from ReplacingMergeTree tables
  • Apply appropriate time windows for session validity (line 1292-1295)
  • Handle errors gracefully with safe defaults (line 1316-1318, 1343-1345)
  • Use parameterized queries to prevent injection

1791-2107: LGTM! Analytics summary enriched with users and chart data.

The enhancements correctly:

  • Track unique users via count(DISTINCT profileId) (lines 1836, 1944, 1975, 2060)
  • Source session duration from the sessions table using FINAL (lines 1843-1850, 1952-1962)
  • Optionally include chart data without breaking existing behavior (lines 1903-1924, 2079-2092)
  • Use appropriate time buckets for different period types (line 1905 for 'all', line 2083 for others)

For projects with many PIDs, the parallel query execution could be resource-intensive. Consider monitoring query performance and potentially adding pagination or result caching.


2109-2175: LGTM! Lightweight chart data method for dashboard cards.

The implementation:

  • Provides a focused API for dashboard use cases (only x and visits)
  • Correctly handles both analytics and custom events (lines 2139-2157, 2159-2174)
  • Uses timezone-aware queries consistently
  • Reuses existing aggregation and extraction logic

2825-2826: Excellent security improvement: Timezone parameterization.

Migrating from string interpolation to parameterized timezone values prevents potential SQL injection vectors. The consistent pattern of:

toTimeZone(created, {timezone:String})

with

query_params: { ...params, timezone: safeTimezone }

ensures proper escaping and type safety across all time-sensitive queries.

This is a critical security hardening that should be highlighted in the PR description.

Also applies to: 2877-2878, 2905-2906, 2931-2932, 2958-2959, 2996-2997, 3029-3030, 3063-3064


3692-3710: LGTM! Error chart extraction includes affected users.

The method correctly:

  • Returns both error count and affected users for richer visualization
  • Safely coerces values with Number() and fallbacks (lines 3704-3705)
  • Follows the established pattern of other chart extraction methods

4147-4186: LGTM! Online user count with appropriate time window.

The implementation:

  • Uses a sensible 5-minute window for "online" status (line 4148)
  • Correctly aggregates across analytics and customEV tables (lines 4157-4167)
  • Uses uniqExact(psid) for accurate distinct counting (line 4156)
  • Handles errors gracefully without disrupting other operations (lines 4182-4185)

4248-4261: Excellent improvement to number sanitization.

The enhanced validation:

  • Handles edge cases: undefined, null, empty strings (line 4249)
  • Rejects non-finite values like NaN and Infinity (line 4255)
  • Enforces ClickHouse LIMIT/OFFSET constraints: non-negative integers (line 4260)

This prevents query errors from malformed input while maintaining type safety.


4361-4396: Verify refund amount sign convention.

The revenue UNION clause (lines 4361-4396) includes both sales and refunds in the session pageflow, but the amount field is stored as-is without sign adjustment.

Consider whether refunds should be:

  1. Stored with negative amounts in the database
  2. Displayed with a negative sign in the UI
  3. Or clearly marked by the type field alone

Verify with the revenue integration implementation that amount signs are consistent across sale/refund operations and that downstream UI correctly interprets them.


4708-5218: LGTM! Comprehensive profile analytics implementation.

The new profile analytics methods provide:

  • Profile listing with filtering by type (anonymous/identified) - lines 4717-4722
  • Detailed stats aggregating across sessions and events - lines 4806-4938
  • Activity patterns via top pages and calendar views - lines 4940-4998
  • Time-series data with aligned pageviews/events/errors - lines 5066-5103
  • Session drill-down for individual profile analysis - lines 5106-5218

All methods:

  • Use parameterized queries consistently
  • Handle timezone conversions properly
  • Leverage appropriate CTEs for performance
  • Return well-structured data shapes

5581-5586: Error rate calculation handles zero sessions correctly.

The error rate calculation (lines 5581-5586) properly guards against division by zero:

const errorRate = totalSessionsResult?.totalSessions > 0
  ? (Number(errorStatsResult?.affectedSessions || 0) / Number(totalSessionsResult.totalSessions)) * 100
  : 0

This ensures the result is always a valid number even when no sessions exist in the time range.


5220-5288: LGTM! Error listing enhancements.

The improvements add:

  • Affected users tracking via count(DISTINCT profileId) (line 5246)
  • Session impact via count(DISTINCT psid) (line 5247)
  • Timezone-aware timestamps (line 5250, 5280)
  • Proper pagination with typed parameters (lines 5271-5272)

These changes provide richer error impact metrics for better monitoring and prioritization.


904-906: LGTM! Tag filtering extends metadata capabilities.

The tag filtering implementation:

  • Appropriately restricts to analytics and errors data types (lines 904-906)
  • Reuses proven patterns from custom event metadata filtering
  • Handles tag:key, tag:value, and tag:key:value filters consistently (lines 1061-1093)
  • Uses parameterized queries to prevent injection

The symmetry with ev: filters reduces cognitive load and maintenance burden.

Also applies to: 953-955, 1061-1093


4623-4642: Remove concern about abs() usage in refund calculations—it is correct.

Refunds are stored as negative values in the database (both Stripe and Paddle adapters apply -Math.abs(amount)). The abs() function in lines 4627-4628 correctly converts these negative amounts to positive for aggregation and reporting. No changes needed.

Likely an incorrect or invalid review comment.

Comment on lines +1215 to +1234
async generateProfileId(
pid: string,
userAgent: string,
ip: string,
userSupplied?: string,
): Promise<string> {
if (userSupplied) {
const cleanId = userSupplied
.replace(AnalyticsService.PROFILE_PREFIX_ANON, '')
.replace(AnalyticsService.PROFILE_PREFIX_USER, '')
const hash = this.hashToNumericString(`${cleanId}${pid}`)
return `${AnalyticsService.PROFILE_PREFIX_USER}${hash}`
}

const salt = await this.saltService.getSaltForProfile()
const combined = `${userAgent}${ip}${pid}${salt}`
const hash = this.hashToNumericString(combined)

return `${AnalyticsService.PROFILE_PREFIX_ANON}${hash}`
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation for empty user-supplied profile IDs.

When processing userSupplied profile IDs, the code strips prefixes but doesn't validate the result. An attacker could send "anon_" or "usr_" as the profile ID, resulting in an empty cleanId and a hash of just the pid.

🔎 Proposed fix
  async generateProfileId(
    pid: string,
    userAgent: string,
    ip: string,
    userSupplied?: string,
  ): Promise<string> {
    if (userSupplied) {
      const cleanId = userSupplied
        .replace(AnalyticsService.PROFILE_PREFIX_ANON, '')
        .replace(AnalyticsService.PROFILE_PREFIX_USER, '')
+       .trim()
+     
+     if (!cleanId) {
+       throw new BadRequestException('Profile ID cannot be empty after normalization')
+     }
+     
      const hash = this.hashToNumericString(`${cleanId}${pid}`)
      return `${AnalyticsService.PROFILE_PREFIX_USER}${hash}`
    }
    // ... rest
  }
📝 Committable suggestion

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

Suggested change
async generateProfileId(
pid: string,
userAgent: string,
ip: string,
userSupplied?: string,
): Promise<string> {
if (userSupplied) {
const cleanId = userSupplied
.replace(AnalyticsService.PROFILE_PREFIX_ANON, '')
.replace(AnalyticsService.PROFILE_PREFIX_USER, '')
const hash = this.hashToNumericString(`${cleanId}${pid}`)
return `${AnalyticsService.PROFILE_PREFIX_USER}${hash}`
}
const salt = await this.saltService.getSaltForProfile()
const combined = `${userAgent}${ip}${pid}${salt}`
const hash = this.hashToNumericString(combined)
return `${AnalyticsService.PROFILE_PREFIX_ANON}${hash}`
}
async generateProfileId(
pid: string,
userAgent: string,
ip: string,
userSupplied?: string,
): Promise<string> {
if (userSupplied) {
const cleanId = userSupplied
.replace(AnalyticsService.PROFILE_PREFIX_ANON, '')
.replace(AnalyticsService.PROFILE_PREFIX_USER, '')
.trim()
if (!cleanId) {
throw new BadRequestException('Profile ID cannot be empty after normalization')
}
const hash = this.hashToNumericString(`${cleanId}${pid}`)
return `${AnalyticsService.PROFILE_PREFIX_USER}${hash}`
}
const salt = await this.saltService.getSaltForProfile()
const combined = `${userAgent}${ip}${pid}${salt}`
const hash = this.hashToNumericString(combined)
return `${AnalyticsService.PROFILE_PREFIX_ANON}${hash}`
}
🤖 Prompt for AI Agents
In backend/apps/cloud/src/analytics/analytics.service.ts around lines 1215 to
1234, the userSupplied branch strips known prefixes but doesn't verify the
remainder, allowing inputs like "anon_" or "usr_" to produce an empty cleanId
and an unsafe hash; after removing prefixes and trimming whitespace, validate
that cleanId is non-empty (and optionally contains expected characters), and if
it's empty treat the input as not provided (i.e., fall back to the anonymous
path: fetch salt, build combined with userAgent+ip+pid+salt and generate anon
hash) otherwise continue generating the user-prefixed hash from
`${cleanId}${pid}` as before.

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: 0

♻️ Duplicate comments (1)
backend/apps/cloud/src/analytics/analytics.controller.ts (1)

2305-2329: Add rate limiting to public ID generation endpoints.

Both the profile-id and session-id public endpoints lack rate limiting, making them vulnerable to:

  • Bulk enumeration attacks to correlate users across projects
  • Resource exhaustion on the analytics service and Redis
  • Abuse by malicious actors

Apply throttling using @nestjs/throttler (e.g., @UseGuards(ThrottlerGuard) with @Throttle() decorator) or integrate express-rate-limit middleware keyed by IP and user-agent.

🔎 Example implementation with NestJS throttler

At the module level (analytics.module.ts), ensure ThrottlerModule is imported:

import { ThrottlerModule } from '@nestjs/throttler'

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
    // ... other imports
  ],
})

Then apply to the endpoints:

+import { ThrottlerGuard, Throttle } from '@nestjs/throttler'
+
 @Post('profile-id')
 @Public()
+@UseGuards(ThrottlerGuard)
+@Throttle(10, 60) // 10 requests per 60 seconds
 async getOrCreateProfileId(
   @Body() dto: GetProfileIdDto,
   @Headers() headers,
   @Ip() reqIP,
 ): Promise<{ profileId: string | null }> {
   // ...
 }

 @Post('session-id')
 @Public()
+@UseGuards(ThrottlerGuard)
+@Throttle(10, 60)
 async getOrCreateSessionId(
   @Body() dto: GetSessionIdDto,
   @Headers() headers,
   @Ip() reqIP,
 ): Promise<{ sessionId: string | null }> {
   // ...
 }

Also applies to: 2331-2355

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

1830-1840: Consider clarifying options parameter handling.

The defensive parsing handles both string and object types, suggesting that options may arrive as a query-string-serialized JSON. While the current implementation is safe, consider documenting this behavior or using a query string transformation decorator to parse it before reaching the controller.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12c6f70 and aa75e9c.

📒 Files selected for processing (2)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (21 hunks)
  • backend/apps/cloud/src/analytics/dto/get-error-overview.dto.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/apps/cloud/src/analytics/dto/get-error-overview.dto.ts (1)
backend/apps/community/src/analytics/dto/get-error-overview.dto.ts (2)
  • GetErrorOverviewOptions (4-6)
  • GetErrorOverviewDto (8-25)
backend/apps/cloud/src/analytics/analytics.controller.ts (4)
backend/apps/cloud/src/analytics/dto/get-error-overview.dto.ts (2)
  • GetErrorOverviewDto (8-25)
  • GetErrorOverviewOptions (4-6)
web/app/lib/constants/index.ts (1)
  • DEFAULT_TIMEZONE (271-271)
backend/apps/cloud/src/analytics/dto/get-id.dto.ts (2)
  • GetProfileIdDto (5-14)
  • GetSessionIdDto (16-25)
backend/apps/cloud/src/common/utils.ts (1)
  • getIPFromHeaders (323-366)
🔇 Additional comments (7)
backend/apps/cloud/src/analytics/dto/get-error-overview.dto.ts (1)

1-25: LGTM! Type mismatch resolved.

The previous type mismatch between the options field and the ApiProperty example has been properly addressed. The interface GetErrorOverviewOptions is now correctly defined and used as the type for the options property.

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

114-114: LGTM!

The ONLINE_VISITORS_WINDOW_MINUTES constant is well-defined and makes the time window explicit and maintainable.


1043-1096: LGTM! Improved live visitors query.

The time-based filtering using ClickHouse is more reliable than relying solely on Redis TTL. The query properly unions both analytics and customEV tables and filters by the time window.


1119-1130: LGTM! Consistent profileId threading.

The profileId generation and session activity recording are consistently implemented across all event recording endpoints (logError, logCustom, heartbeat, log, noscript). The heartbeat endpoint correctly uses getSessionId and extends the session TTL as intended.

Also applies to: 1258-1269, 1345-1353, 1381-1392, 1527-1537


2277-2288: LGTM! Robust JSON parsing.

The customEvents parsing properly handles the JSON-stringified array format with appropriate error handling and early return for empty arrays.


1979-2061: LGTM! Well-structured profile endpoints.

The new profile-related endpoints (getProfiles, getProfile, getProfileSessions) are properly implemented with:

  • Appropriate authentication and authorization checks
  • Consistent pagination patterns
  • Efficient parallel queries using Promise.all in the profile details endpoint

Also applies to: 2063-2133, 2135-2217


1892-1949: LGTM! Error sessions endpoint.

The getErrorSessions endpoint is properly implemented with appropriate validation and pagination (max 50 sessions, which is reasonable for error-specific queries).

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: 0

🧹 Nitpick comments (1)
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1)

50-56: Consider adding format validation for date parameters.

The from and to fields accept any string without validating date format. Consider adding @IsDateString() or @Matches() with a date pattern to catch invalid formats at the DTO level rather than downstream.

🔎 Example validation enhancement
  @ApiProperty({ required: false })
  @IsOptional()
+ @IsDateString()
  from?: string

  @ApiProperty({ required: false })
  @IsOptional()
+ @IsDateString()
  to?: string

Note: Ensure IsDateString is imported from class-validator.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa75e9c and 09de7c1.

📒 Files selected for processing (1)
  • backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1)
web/app/lib/constants/index.ts (1)
  • DEFAULT_TIMEZONE (271-271)
⏰ 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). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (2)

1-16: Imports look good.

All necessary decorators and utilities are properly imported. The DEFAULT_TIMEZONE import from the user entity is appropriate for backend DTOs.


59-91: Well-structured pagination DTO.

The use of PickType to inherit common fields from GetDataDto follows DRY principles. The pagination parameters (take, skip) have appropriate validation and type transformation for query parameter handling.

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: 3

♻️ Duplicate comments (1)
backend/apps/cloud/src/integrations/discord/discord.service.ts (1)

16-27: Subdomain validation issue resolved.

The hostname validation now correctly accepts Discord subdomains by checking !host.endsWith('.discord.com') on line 22. This addresses the concern raised in the previous review about rejecting legitimate Discord webhook URLs from canary and PTB environments.

🧹 Nitpick comments (5)
backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (2)

128-145: Sequential processing may be slow for large transaction volumes.

The sync loop processes transactions and adjustments one at a time with await inside the loop. For projects with many transactions, this could be slow.

Consider batching with Promise.all using chunked arrays for better performance, while being mindful of rate limits.

🔎 Example batched approach
-     for (const transaction of transactions) {
-       await this.processTransaction(projectId, transaction, currency)
-       syncedCount++
-     }
+     const BATCH_SIZE = 10
+     for (let i = 0; i < transactions.length; i += BATCH_SIZE) {
+       const batch = transactions.slice(i, i + BATCH_SIZE)
+       await Promise.all(
+         batch.map(tx => this.processTransaction(projectId, tx, currency)),
+       )
+       syncedCount += batch.length
+     }

346-351: Consider handling multiple line items for product attribution.

The code only extracts productName and productId from the first line item. If a transaction has multiple products, only the first is recorded. This may be intentional, but worth noting for analytics accuracy.

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

222-253: Silent failure on re-sync may confuse users.

When the re-sync fails after a currency update, the error is logged but the response still returns { success: true }. Users might expect their historical data to be re-synced with the new currency. Consider returning a partial success status or warning.

🔎 Proposed approach
    } catch (error) {
      this.logger.error(
        { error, pid },
        'Failed to trigger re-sync after currency update',
      )
-     // Don't throw here, as currency was updated successfully
    }

-   return { success: true }
+   return { 
+     success: true, 
+     warning: error ? 'Currency updated but re-sync failed. Data will sync on next scheduled run.' : undefined 
+   }
backend/apps/cloud/src/goal/goal.service.ts (1)

90-92: Consider using create() before save() for clearer semantics.

The save() method will update an existing entity if an id is present in goalData, which may not be the intended behavior for a method named create. Consider using the repository's create() method first to instantiate the entity, then save() it, or use insert() for stricter create-only semantics.

🔎 Suggested refactor
 async create(goalData: Partial<Goal>): Promise<Goal> {
-  return this.goalsRepository.save(goalData)
+  const goal = this.goalsRepository.create(goalData)
+  return this.goalsRepository.save(goal)
 }
backend/apps/cloud/src/experiment/experiment.controller.ts (1)

119-126: Replace @ts-expect-error with proper typing.

The @ts-expect-error directive suppresses TypeScript errors rather than addressing the underlying type mismatch. The paginated result type should be extended or a proper transformation type should be defined to handle the mapping from entity to DTO shape.

🔎 Suggested approach

Define a proper return type for the paginated results or cast to the correct type after transformation:

+  type PaginatedExperimentDto = {
+    results: ExperimentDto[]
+    total: number
+  }
+
   const result = await this.experimentService.paginate(
     { take: safeTake, skip: safeSkip },
     projectId,
   )

-  // @ts-expect-error
-  result.results = _map(result.results, experiment => ({
+  const transformedResult: PaginatedExperimentDto = {
+    ...result,
+    results: _map(result.results, experiment => ({
-    ..._omit(experiment, ['project', 'goal', 'featureFlag']),
-    pid: projectId,
-    goalId: experiment.goal?.id || null,
-    featureFlagId: experiment.featureFlag?.id || null,
-  }))
+      ..._omit(experiment, ['project', 'goal', 'featureFlag']),
+      pid: projectId,
+      goalId: experiment.goal?.id || null,
+      featureFlagId: experiment.featureFlag?.id || null,
+    })),
+  }

-  return result
+  return transformedResult
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09de7c1 and d6d11a5.

📒 Files selected for processing (8)
  • backend/apps/cloud/src/experiment/experiment.controller.ts (1 hunks)
  • backend/apps/cloud/src/goal/entity/goal.entity.ts (1 hunks)
  • backend/apps/cloud/src/goal/goal.service.ts (1 hunks)
  • backend/apps/cloud/src/integrations/discord/discord.service.ts (1 hunks)
  • backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/update-revenue-currency.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.controller.ts (1 hunks)
  • backend/apps/cloud/src/user/dto/update-user.dto.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/apps/cloud/src/goal/entity/goal.entity.ts
🧰 Additional context used
🧬 Code graph analysis (2)
backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (2)
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1)
  • RevenueTransaction (18-35)
web/app/api/index.ts (1)
  • RevenueProvider (2470-2470)
backend/apps/cloud/src/goal/goal.service.ts (2)
backend/apps/community/src/goal/entity/goal.entity.ts (1)
  • Goal (16-26)
backend/apps/community/src/common/pagination/pagination.results.interface.ts (1)
  • PaginationOptionsInterface (8-11)
🔇 Additional comments (16)
backend/apps/cloud/src/revenue/adapters/paddle.adapter.ts (4)

89-118: Timeout handling properly implemented.

The validateApiKey method now correctly uses AbortController with timeout, addresses the abort error specifically, and clears the timeout in the finally block. This addresses the previous review feedback.


198-225: Timeout and pagination handling is robust.

The paginated fetch correctly implements timeout with AbortController, properly clears the timer in finally, distinguishes abort errors from other errors, and correctly handles cursor-based pagination. This addresses the previous review feedback about missing timeouts.


375-409: Refund processing correctly handles negative amounts.

The processAdjustment method properly negates the amount for refunds and stores the original transaction ID in metadata for traceability.


329-329: Remove this comment—the code correctly handles Paddle's currency format.

Paddle returns monetary values in the lowest denomination (cents for USD), so dividing by 100 is the correct approach.

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

4-13: DTO validation is appropriate for currency codes.

The @Length(3, 3) constraint correctly validates ISO 4217 currency codes. The previous filename mismatch issue has been resolved.

Consider adding @Transform to uppercase the input or @Matches for alphabetic validation if stricter enforcement is needed, but current validation is acceptable.

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

104-131: Analytics tracking for Paddle provider is now included.

The trackCustom call for REVENUE_SETUP with provider: 'paddle' is correctly added before the return statement. This addresses the previous review feedback.


296-314: Good use of shared analytics utilities.

The code correctly uses getLowestPossibleTimeBucket, getSafeTimezone, getGroupFromTo, and generateXAxis for consistent time-based analytics calculations across the application.

backend/apps/cloud/src/user/dto/update-user.dto.ts (2)

35-40: LGTM! Discord webhook validation now supports subdomains.

The updated regex pattern correctly allows Discord subdomains (e.g., canary.discord.com, ptb.discord.com) via the (?:[a-zA-Z0-9-]+\.)* prefix, and aligns well with the runtime validation in the Discord service.


27-27: The Slack webhook URL regex pattern correctly uses alphanumeric characters only.

Slack webhook URLs follow the format https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX, where Slack object IDs consist of a prefix letter followed by an alphanumerical value. Team/workspace IDs (T prefix) are 11 characters long, and all documented examples (T12345, T04FTGB9SD3, B02ETC8TFMX) contain only alphanumeric characters. The regex pattern using [a-zA-Z0-9] is correct—Slack workspace and service IDs do not contain underscores.

backend/apps/cloud/src/integrations/discord/discord.service.ts (3)

29-43: LGTM! Message normalization and truncation handled correctly.

The message handling properly:

  • Normalizes non-string messages to strings via JSON.stringify or String()
  • Truncates content at 1900 characters with appropriate buffer under Discord's 2000 character limit
  • Appends ellipsis to indicate truncation

45-51: LGTM! Good security practices in HTTP request configuration.

The 10-second timeout and maxRedirects: 0 are appropriate defensive measures to prevent hanging connections and SSRF redirect attacks.


53-57: LGTM! Enhanced error logging provides better diagnostics.

The error handling now includes HTTP status codes when available, which will help with debugging webhook delivery issues.

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

1-19: Good typing improvements!

The imports now include UpdateResult and DeleteResult from TypeORM, and the service structure follows NestJS best practices. This addresses the previous review feedback about replacing any return types.


21-62: Excellent fix for the array handling issue!

The paginate method now properly normalizes the where parameter into an array (line 36) and uses flatMap (lines 42-45) to correctly handle search across multiple base conditions. This addresses the previous review comment about spreading where when it might already be an array.


94-100: Good fix for return types!

The update and delete methods now use proper TypeORM result types (UpdateResult and DeleteResult) instead of any. This improves type safety and addresses the previous review feedback.


83-88: The nested where syntax is properly supported by the Goal entity.

The project field in the Goal entity is correctly decorated with @ManyToOne(() => Project, project => project.goals) and @JoinColumn(), enabling the nested filtering syntax { project: { id: projectId } } used in the findByProject method to work as expected.

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: 0

♻️ Duplicate comments (3)
backend/apps/cloud/src/revenue/revenue.controller.ts (3)

104-131: Paddle analytics tracking has been added.

The previous review comment about missing analytics tracking for Paddle has been addressed. The trackCustom call is now present at lines 123-128.


378-422: Revenue configuration check has been added.

The previous review comment about missing revenue configuration check has been addressed. The check is now present at lines 398-402.


354-356: Still missing revenue configuration check.

This endpoint does not verify that revenue tracking is configured before querying transactions. Unlike getRevenueStats (lines 290-294) and getRevenueBreakdown (lines 398-402), which both check for API keys, this method may return empty results without a clear error message.

🔎 Proposed fix
    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)

Note: This issue was flagged in a previous review but remains unaddressed.

🧹 Nitpick comments (2)
backend/apps/cloud/src/revenue/revenue.controller.ts (2)

398-404: Consider reordering checks for consistency.

The authorization check (allowedToView) happens after the revenue configuration check, which differs from getRevenueStats where authorization is checked first (line 288) before configuration (lines 290-294). For consistency and to fail fast on authorization, consider checking allowedToView before validating revenue configuration.

🔎 Proposed reordering
    if (_isEmpty(project)) {
      throw new NotFoundException('Project not found')
    }

+   this.projectService.allowedToView(project, userId)
+
    if (!project.paddleApiKeyEnc && !project.stripeApiKeyEnc) {
      throw new BadRequestException(
        'Revenue tracking is not configured for this project',
      )
    }

-   this.projectService.allowedToView(project, userId)
-
    const safeTimezone = this.analyticsService.getSafeTimezone(dto.timezone)

304-304: Validate timeBucket against TimeBucketType enum instead of using type assertions.

The as any casts on lines 304, 310, 362, and 412 hide a real type mismatch: dto.timeBucket is typed as string (from user input), but getGroupFromTo() and generateXAxis() expect TimeBucketType enum values. When users pass timeBucket via the API, there's no guarantee it matches valid enum values. Add a validation step or conversion function to ensure timeBucket is properly typed before passing to AnalyticsService methods, eliminating the need for type assertions.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6d11a5 and 445841a.

📒 Files selected for processing (1)
  • backend/apps/cloud/src/revenue/revenue.controller.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/apps/cloud/src/revenue/revenue.controller.ts (7)
backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts (1)
  • ConnectRevenueDto (6-39)
backend/apps/cloud/src/common/analytics.ts (1)
  • trackCustom (10-23)
web/app/utils/analytics.ts (1)
  • trackCustom (173-184)
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1)
  • STRIPE_REQUIRED_PERMISSIONS (46-55)
backend/apps/cloud/src/revenue/dto/update-revenue-currency.dto.ts (1)
  • UpdateRevenueCurrencyDto (4-13)
backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (2)
  • GetRevenueDto (12-42)
  • GetRevenueTransactionsDto (44-71)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getLowestPossibleTimeBucket (205-246)
⏰ 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). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
backend/apps/cloud/src/revenue/revenue.controller.ts (2)

222-253: Intentional error handling for re-sync is appropriate.

The try-catch block correctly allows the currency update to succeed even if the immediate re-sync fails. This is a good UX decision since the currency is persisted and subsequent syncs can pick up the new value.


318-333: Good use of parallel data fetching.

Using Promise.all to fetch stats and chart data concurrently is an efficient pattern that minimizes response time.

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: 1

🧹 Nitpick comments (3)
backend/apps/cloud/src/experiment/experiment.controller.ts (1)

119-126: Consider adding a proper type annotation.

The @ts-expect-error comment suggests a type mismatch. Consider explicitly typing the mapped results or using a type assertion to avoid suppressing type errors.

🔎 Potential improvement
-    // @ts-expect-error
-    result.results = _map(result.results, experiment => ({
+    result.results = _map(result.results, experiment => ({
       ..._omit(experiment, ['project', 'goal', 'featureFlag']),
       pid: projectId,
       goalId: experiment.goal?.id || null,
       featureFlagId: experiment.featureFlag?.id || null,
-    }))
+    })) as any

Or better yet, define an explicit return type for the transformation.

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

162-193: Add explicit return type annotation.

The method should have an explicit return type of Promise<void> to match the 204 No Content response status and improve type safety.

🔎 Proposed fix
   @Delete('/:pid/revenue/disconnect')
   @Auth()
   @ApiResponse({ status: 204 })
   async disconnectRevenue(
     @CurrentUserId() userId: string,
     @Param('pid') pid: string,
     @Headers() headers: Record<string, string>,
     @Ip() requestIp: string,
-  ) {
+  ): Promise<void> {

195-256: Add @apiresponse decorator for API documentation consistency.

Unlike other endpoints in this controller, updateCurrency is missing an @ApiResponse decorator. Adding it ensures consistent OpenAPI/Swagger documentation.

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

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 445841a and d4b6ecd.

📒 Files selected for processing (3)
  • backend/apps/cloud/src/experiment/experiment.controller.ts (1 hunks)
  • backend/apps/cloud/src/experiment/experiment.service.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.controller.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/apps/cloud/src/experiment/experiment.controller.ts (5)
backend/apps/cloud/src/common/utils.ts (1)
  • getIPFromHeaders (323-366)
web/app/api/index.ts (4)
  • ExposureTrigger (1520-1520)
  • Experiment (1535-1556)
  • ExperimentStatus (1518-1518)
  • ExperimentVariant (1526-1533)
backend/apps/cloud/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlagType (15-15)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getLowestPossibleTimeBucket (205-246)
backend/apps/cloud/src/experiment/bayesian.ts (1)
  • calculateBayesianProbabilities (118-182)
⏰ 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). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (23)
backend/apps/cloud/src/experiment/experiment.service.ts (5)

1-15: LGTM!

Clean dependency injection setup following NestJS best practices.


17-40: LGTM!

Pagination implementation correctly uses QueryBuilder with eager loading to avoid N+1 queries.


42-73: LGTM!

Query methods provide clean abstractions over TypeORM repository operations.


75-114: LGTM!

CRUD operations are implemented correctly for both Experiment and ExperimentVariant entities.


116-140: LGTM!

Transactional variant recreation ensures atomicity, preventing partial updates if any operation fails.

backend/apps/cloud/src/experiment/experiment.controller.ts (14)

1-72: LGTM!

Clean imports and well-defined constants.


73-84: LGTM!

Constructor correctly injects all required services.


130-159: LGTM!

Proper permission checks and clean DTO mapping.


161-220: LGTM!

Comprehensive validation including rate limiting, permissions, subscription checks, and experiment count limits.


229-249: LGTM!

Critical variant validation ensures experiment integrity (minimum 2 variants, exactly 1 control, 100% rollout total).


251-291: LGTM!

Comprehensive validation of goal and feature flag relationships with clear error messages.


293-346: LGTM!

Clean experiment creation with proper error handling, analytics tracking, and feature flag linking.


349-521: LGTM!

Robust update logic with state machine enforcement, atomic variant updates, and comprehensive validation.


523-568: LGTM!

Proper cleanup logic that handles feature flag deletion or unlinking based on the feature flag mode.


570-668: LGTM!

Comprehensive start logic with state validation, goal requirement enforcement, and automatic feature flag creation when needed.


670-782: LGTM!

Clean lifecycle management with proper state transitions and feature flag handling.


784-944: LGTM!

Comprehensive results endpoint with proper ClickHouse queries, error handling, and support for various goal types and filters.


946-1042: LGTM!

Robust Bayesian analysis with proper guard against empty arrays and clear winner determination logic.


1044-1340: LGTM!

Well-designed chart generation with cumulative data approach and comprehensive time bucket handling. The comment explaining the approach (lines 1063-1068) is particularly helpful.

backend/apps/cloud/src/revenue/revenue.controller.ts (4)

75-160: LGTM! Analytics tracking now covers both providers.

The analytics tracking for both Paddle and Stripe providers is correctly implemented. The previous concern about missing Paddle analytics has been addressed.


269-336: LGTM! Revenue configuration check is properly implemented.

The revenue configuration validation has been correctly added, addressing the previous concern. The endpoint now consistently checks for provider configuration before processing analytics requests.


338-382: LGTM! Revenue configuration check is properly implemented.

The revenue configuration validation has been correctly added, addressing the previous concern. The endpoint now consistently checks for provider configuration before querying transactions.


384-424: LGTM! Revenue configuration check is properly implemented.

The revenue configuration validation has been correctly added, addressing the previous concern. The endpoint now consistently checks for provider configuration before generating breakdown data.

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: 4

🧹 Nitpick comments (4)
backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1)

65-70: Consider enum validation for type filter.

The description documents specific values (sale, refund, subscription), but the field only validates as a string. Using @IsIn(['sale', 'refund', 'subscription']) would reject invalid values at the API boundary rather than silently ignoring them in the service layer.

🔎 Proposed enhancement
   @ApiPropertyOptional({
     description: 'Filter by type (sale, refund, subscription)',
   })
   @IsOptional()
-  @IsString()
+  @IsIn(['sale', 'refund', 'subscription'])
   type?: string

Add the import:

 import {
   IsNotEmpty,
   IsString,
   IsOptional,
   IsInt,
   Min,
   Max,
   IsEnum,
+  IsIn,
 } from 'class-validator'
backend/apps/cloud/src/revenue/revenue.controller.ts (2)

222-256: Consider returning sync status to the caller.

The re-sync error is logged but the response always returns { success: true }. While the currency update itself succeeded, the user has no visibility into whether the sync completed. Consider returning additional status information.

🔎 Proposed enhancement
     } catch (error) {
       this.logger.error(
         { error, pid },
         'Failed to trigger re-sync after currency update',
       )
       // Don't throw here, as currency was updated successfully
+      return { success: true, syncTriggered: false }
     }

-    return { success: true }
+    return { success: true, syncTriggered: true }
   }

290-294: Consider extracting shared revenue validation logic.

The revenue configuration check and time range computation logic is duplicated across getRevenueStats, getRevenueTransactions, and getRevenueBreakdown. This could be extracted to a private helper method or a NestJS guard for cleaner code.

🔎 Example helper extraction
private validateRevenueConfigured(project: Project): void {
  if (!project.paddleApiKeyEnc && !project.stripeApiKeyEnc) {
    throw new BadRequestException(
      'Revenue tracking is not configured for this project',
    )
  }
}

Also applies to: 356-360, 402-406

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

103-109: Consider optimizing project permission check.

Fetching the full project with getFullProject may load unnecessary relations or data just to verify access permissions. If allowedToView only needs basic project metadata and ownership information, consider using a lighter query or a dedicated permission-check method.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4b6ecd and 73764ff.

📒 Files selected for processing (3)
  • backend/apps/cloud/src/experiment/experiment.controller.ts (1 hunks)
  • backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1 hunks)
  • backend/apps/cloud/src/revenue/revenue.controller.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/apps/cloud/src/experiment/experiment.controller.ts (4)
backend/apps/cloud/src/common/utils.ts (1)
  • getIPFromHeaders (323-366)
backend/apps/cloud/src/common/analytics.ts (1)
  • trackCustom (10-23)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getLowestPossibleTimeBucket (205-246)
backend/apps/cloud/src/experiment/bayesian.ts (1)
  • calculateBayesianProbabilities (118-182)
backend/apps/cloud/src/revenue/revenue.controller.ts (6)
backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts (1)
  • ConnectRevenueDto (6-39)
backend/apps/cloud/src/common/analytics.ts (1)
  • trackCustom (10-23)
backend/apps/cloud/src/revenue/interfaces/revenue.interface.ts (1)
  • STRIPE_REQUIRED_PERMISSIONS (46-55)
backend/apps/cloud/src/revenue/dto/update-revenue-currency.dto.ts (1)
  • UpdateRevenueCurrencyDto (4-13)
backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (2)
  • GetRevenueDto (14-47)
  • GetRevenueTransactionsDto (49-76)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getLowestPossibleTimeBucket (205-246)
🔇 Additional comments (12)
backend/apps/cloud/src/revenue/dto/get-revenue.dto.ts (1)

1-47: LGTM! Well-structured base DTO with proper validation.

The GetRevenueDto class correctly uses @IsEnum(TimeBucketType) for the timeBucket field, which properly types the parameter and eliminates the need for as any casts in the controller. The validation decorators are appropriately applied.

backend/apps/cloud/src/revenue/revenue.controller.ts (5)

54-73: LGTM! Proper access control and error handling.

The endpoint correctly validates project existence and uses allowedToView for permission checks before returning revenue status.


79-160: LGTM! Both providers now have analytics tracking.

The connectRevenue method properly validates API keys for both Paddle and Stripe providers, with trackCustom calls appropriately placed for each path. This addresses the previous review concern about missing Paddle tracking.


269-336: LGTM! Well-structured analytics endpoint with proper guards.

The endpoint correctly:

  • Validates project existence and user permissions
  • Checks revenue tracking configuration before querying
  • Uses properly typed timeBucket from the DTO (resolving the previous as any cast concern)
  • Parallelizes stats and chart retrieval with Promise.all

338-382: LGTM! Revenue transactions endpoint properly guarded.

The revenue configuration check is now in place, addressing the previous review comment. The pagination defaults are correctly applied.


384-424: LGTM! Breakdown endpoint follows consistent patterns.

The endpoint includes the revenue configuration guard and follows the same access control patterns as the other endpoints.

backend/apps/cloud/src/experiment/experiment.controller.ts (6)

846-875: LGTM!

The exposures query construction properly uses parameterized queries with typed placeholders (e.g., {pid:FixedString(12)}), which prevents SQL injection vulnerabilities.


909-945: LGTM!

The conversions query construction uses proper parameterization for all dynamic values including metadata filters, preventing SQL injection. The dynamic table and column selection based on goalType is safe because it's constrained to known values from the goal entity.


1005-1010: LGTM!

The guard condition if (variantResults.length > 0) properly prevents the runtime error from calling reduce on an empty array, addressing the concern from previous reviews.


1064-1069: Excellent documentation.

The comment clearly explains why the cumulative approach is necessary to ensure consistency between table results and chart data. This prevents a common pitfall of summing per-bucket uniques when profiles can appear in multiple buckets.


176-177: Verify rate limit consistency across experiment endpoints.

Different endpoints use different rate limits:

  • create: 20/hour (line 176)
  • update: 30/hour (line 365)
  • delete: 20/hour (line 539)
  • lifecycle: 30/hour (lines 585, 685, 739)

Ensure these limits align with intended usage patterns and are consistent with your rate-limiting policy. Consider whether update and lifecycle operations should have higher limits than creation/deletion.


71-72: No inconsistency found. The FEATURE_FLAG_KEY_REGEX pattern in the controller (/^[a-zA-Z0-9_-]+$/) matches the KEY_REGEX validation in the experiment DTOs (/^[a-zA-Z0-9_-]+$/). Both use identical patterns and error messages across create, update, and validation operations.

Likely an incorrect or invalid review comment.

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: 2

🧹 Nitpick comments (3)
backend/apps/cloud/src/experiment/experiment.service.ts (1)

17-40: Add pagination bounds validation for consistency and safety.

Unlike FeatureFlagService.paginate, this method lacks bounds validation. A caller could pass an excessively large take value, and take: 0 would be treated as falsy and default to 100.

🔎 Suggested fix
   async paginate(
     options: PaginationOptionsInterface,
     projectId: string,
   ): Promise<Pagination<Experiment>> {
+    const safeTake =
+      typeof options.take === 'number' && Number.isFinite(options.take)
+        ? Math.min(Math.max(options.take, 0), 100)
+        : 100
+    const safeSkip =
+      typeof options.skip === 'number' && Number.isFinite(options.skip)
+        ? Math.max(options.skip, 0)
+        : 0
+
     const queryBuilder = this.experimentRepository
       .createQueryBuilder('experiment')
       .leftJoinAndSelect('experiment.project', 'project')
       .leftJoinAndSelect('experiment.variants', 'variants')
       .leftJoinAndSelect('experiment.goal', 'goal')
       .leftJoinAndSelect('experiment.featureFlag', 'featureFlag')
       .where('project.id = :projectId', { projectId })

     queryBuilder
       .orderBy('experiment.created', 'DESC')
-      .take(options.take || 100)
-      .skip(options.skip || 0)
+      .take(safeTake)
+      .skip(safeSkip)

     const [results, total] = await queryBuilder.getManyAndCount()
backend/apps/cloud/src/experiment/experiment.controller.ts (2)

591-601: Consider wrapping delete operations in a transaction.

The feature flag update (unlinking) and experiment deletion are performed sequentially without a transaction. If the experiment deletion fails after the feature flag is updated, the flag will be left in an inconsistent state (experimentId = null while the experiment still exists).

🔎 Suggested fix
-    if (experiment.featureFlag) {
-      if (experiment.featureFlagMode === FeatureFlagMode.CREATE) {
-        await this.featureFlagService.delete(experiment.featureFlag.id)
-      } else {
-        await this.featureFlagService.update(experiment.featureFlag.id, {
-          experimentId: null,
-        })
-      }
-    }
-
-    await this.experimentService.delete(id)
+    await this.dataSource.transaction(async transactionalEntityManager => {
+      if (experiment.featureFlag) {
+        if (experiment.featureFlagMode === FeatureFlagMode.CREATE) {
+          await this.featureFlagService.delete(experiment.featureFlag.id)
+        } else {
+          await this.featureFlagService.update(
+            experiment.featureFlag.id,
+            { experimentId: null },
+            transactionalEntityManager,
+          )
+        }
+      }
+
+      await this.experimentService.delete(id)
+    })

764-772: Consider using transaction for consistency.

The feature flag update and experiment status update are not wrapped in a transaction. While the impact is lower than in create/delete flows (a retry would fix the state), wrapping in a transaction would ensure consistency if either operation fails.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73764ff and 57b329c.

📒 Files selected for processing (3)
  • backend/apps/cloud/src/experiment/experiment.controller.ts (1 hunks)
  • backend/apps/cloud/src/experiment/experiment.service.ts (1 hunks)
  • backend/apps/cloud/src/feature-flag/feature-flag.service.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/apps/cloud/src/experiment/experiment.service.ts (2)
web/app/api/index.ts (2)
  • Experiment (1535-1556)
  • ExperimentVariant (1526-1533)
backend/apps/community/src/common/pagination/pagination.results.interface.ts (1)
  • PaginationOptionsInterface (8-11)
backend/apps/cloud/src/feature-flag/feature-flag.service.ts (2)
backend/apps/community/src/feature-flag/entity/feature-flag.entity.ts (1)
  • FeatureFlag (10-15)
backend/apps/community/src/common/pagination/pagination.results.interface.ts (1)
  • PaginationOptionsInterface (8-11)
🪛 GitHub Actions: Pull Request Validation Workflow
backend/apps/cloud/src/experiment/experiment.service.ts

[error] 3-3: prettier/prettier: Replace FindManyOptions, FindOneOptions, Repository, EntityManager with ⏎ FindManyOptions,⏎ FindOneOptions,⏎ Repository,⏎ EntityManager,⏎

🪛 GitHub Check: Backend Checks (22.x.x)
backend/apps/cloud/src/experiment/experiment.service.ts

[failure] 3-3:
Replace ·FindManyOptions,·FindOneOptions,·Repository,·EntityManager· with ⏎··FindManyOptions,⏎··FindOneOptions,⏎··Repository,⏎··EntityManager,⏎

backend/apps/cloud/src/feature-flag/feature-flag.service.ts

[failure] 3-3:
Replace ·FindManyOptions,·FindOneOptions,·Repository,·EntityManager· with ⏎··FindManyOptions,⏎··FindOneOptions,⏎··Repository,⏎··EntityManager,⏎

⏰ 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). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (12)
backend/apps/cloud/src/feature-flag/feature-flag.service.ts (3)

18-53: LGTM!

The pagination implementation is well-designed with proper bounds checking (capping take at 100, ensuring non-negative skip), parameterized queries for security, and case-insensitive search support.


88-111: LGTM!

The create/update methods correctly support optional EntityManager for transactional operations, enabling atomic multi-entity updates when called from controllers or other services.


113-133: LGTM!

Clean delegation to shared evaluation logic promotes code reuse and maintains single responsibility.

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

75-98: LGTM!

The create/update/delete methods follow the same pattern as FeatureFlagService with proper transaction support via optional EntityManager.


129-160: LGTM!

Proper transactional handling for atomic variant recreation. The method correctly supports both standalone transactions and nested usage within an existing transaction context.

backend/apps/cloud/src/experiment/experiment.controller.ts (7)

73-88: LGTM!

Well-organized constants and comprehensive dependency injection for the controller's responsibilities.


90-138: LGTM!

Proper authorization, pagination bounds validation, and clean DTO mapping. The controller correctly sanitizes pagination inputs before passing to the service.


335-354: LGTM!

The transactional block correctly wraps both the experiment creation and feature flag update, ensuring atomicity. This addresses the previously identified concern about inconsistent state.


429-545: LGTM!

The update flow correctly uses a transaction to wrap variant recreation and feature flag updates atomically. The state-machine checks prevent modifications to running/completed experiments.


1062-1070: LGTM!

The empty array guard before reduce prevents the runtime error that was flagged in previous reviews. Good fix.


969-984: LGTM!

The ClickHouse queries are properly parameterized with typed placeholders. Table and column names are derived from controlled enum values, preventing injection risks. The redundant NULL check from the previous review was addressed.


1124-1130: Good documentation on the deduplication strategy.

The comment clearly explains why first-exposure/first-conversion bucketing is used instead of summing per-bucket uniques, ensuring consistency between chart data and table totals.

Comment on lines +1 to +6
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { Experiment } from './entity/experiment.entity'
import { ExperimentVariant } from './entity/experiment-variant.entity'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Format multi-line imports per project style.

Same formatting issue as flagged by the pipeline - the TypeORM imports should be split across multiple lines.

🔎 Suggested fix
 import { Injectable } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
-import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
+import {
+  FindManyOptions,
+  FindOneOptions,
+  Repository,
+  EntityManager,
+} from 'typeorm'
 import { Pagination, PaginationOptionsInterface } from '../common/pagination'
📝 Committable suggestion

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

Suggested change
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { Experiment } from './entity/experiment.entity'
import { ExperimentVariant } from './entity/experiment-variant.entity'
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import {
FindManyOptions,
FindOneOptions,
Repository,
EntityManager,
} from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { Experiment } from './entity/experiment.entity'
import { ExperimentVariant } from './entity/experiment-variant.entity'
🧰 Tools
🪛 GitHub Actions: Pull Request Validation Workflow

[error] 3-3: prettier/prettier: Replace FindManyOptions, FindOneOptions, Repository, EntityManager with ⏎ FindManyOptions,⏎ FindOneOptions,⏎ Repository,⏎ EntityManager,⏎

🪛 GitHub Check: Backend Checks (22.x.x)

[failure] 3-3:
Replace ·FindManyOptions,·FindOneOptions,·Repository,·EntityManager· with ⏎··FindManyOptions,⏎··FindOneOptions,⏎··Repository,⏎··EntityManager,⏎

🤖 Prompt for AI Agents
In backend/apps/cloud/src/experiment/experiment.service.ts around lines 1 to 6,
the TypeORM import is currently a single-line list; split the named imports from
'typeorm' across multiple lines to match project style (e.g. one import per line
or grouped consistently), keeping the import source the same and preserving
alphabetical/order if required, then run the linter/formatter to ensure it
passes pipeline checks.

Comment on lines +1 to +9
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { FeatureFlag } from './entity/feature-flag.entity'
import {
evaluateFlag as sharedEvaluateFlag,
evaluateFlags as sharedEvaluateFlags,
} from './evaluation'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Format multi-line imports per project style.

The static analysis/pipeline failure indicates that the TypeORM imports on line 3 should be formatted across multiple lines to match the project's Prettier configuration.

🔎 Suggested fix
 import { Injectable } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
-import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
+import {
+  FindManyOptions,
+  FindOneOptions,
+  Repository,
+  EntityManager,
+} from 'typeorm'
 import { Pagination, PaginationOptionsInterface } from '../common/pagination'
📝 Committable suggestion

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

Suggested change
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { FindManyOptions, FindOneOptions, Repository, EntityManager } from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { FeatureFlag } from './entity/feature-flag.entity'
import {
evaluateFlag as sharedEvaluateFlag,
evaluateFlags as sharedEvaluateFlags,
} from './evaluation'
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import {
FindManyOptions,
FindOneOptions,
Repository,
EntityManager,
} from 'typeorm'
import { Pagination, PaginationOptionsInterface } from '../common/pagination'
import { FeatureFlag } from './entity/feature-flag.entity'
import {
evaluateFlag as sharedEvaluateFlag,
evaluateFlags as sharedEvaluateFlags,
} from './evaluation'
🧰 Tools
🪛 GitHub Check: Backend Checks (22.x.x)

[failure] 3-3:
Replace ·FindManyOptions,·FindOneOptions,·Repository,·EntityManager· with ⏎··FindManyOptions,⏎··FindOneOptions,⏎··Repository,⏎··EntityManager,⏎

🤖 Prompt for AI Agents
In backend/apps/cloud/src/feature-flag/feature-flag.service.ts around lines 1 to
9, the TypeORM import on line 3 must be reformatted to match project style by
splitting the multiple named imports across multiple lines; change the
single-line import "import { FindManyOptions, FindOneOptions, Repository,
EntityManager } from 'typeorm'" into a multiline import listing each named
export on its own line (or one per line consistent with the repo style), keep
the surrounding imports intact, and run the project's formatter/Prettier to
ensure the file passes static analysis.

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.

2 participants