Skip to content

Comments

Profiles / MAU#425

Merged
Blaumaus merged 31 commits intoswetrix-revampfrom
feature/adjustable-salt-setting
Dec 5, 2025
Merged

Profiles / MAU#425
Blaumaus merged 31 commits intoswetrix-revampfrom
feature/adjustable-salt-setting

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Nov 30, 2025

#421

  1. Introduce Profiles
  2. Derive session ID from salt + pid + request info, instead of storing key-value pair in Redis

TODOs:

  • Enable Redis persistence config setting OR store salts in MySQL and proxy through Redis

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

  • New Features

    • Added comprehensive user profile analytics with new profiles tab displaying visitor lists, sessions, activity calendars, and engagement metrics.
    • Added ability to configure unique visitor tracking period (daily, weekly, or monthly rotation).
    • Added profile view with detailed user information, top pages, activity timeline, and session history with pagination.
  • Documentation

    • Updated localization strings to support new profile analytics interface and visitor tracking settings.

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

@Blaumaus Blaumaus self-assigned this Nov 30, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 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

Refactored session management from Redis-based hashing to salted PSID derivation with SaltService. Introduced profile management system with ClickHouse-backed sessions table. Added profile-related endpoints and DTOs. Migrated analytics data to include profileId. Added new UI components for user/profile tracking.

Changes

Cohort / File(s) Summary
Session & Salt Management
backend/apps/cloud/src/analytics/salt.service.ts, backend/apps/community/src/analytics/salt.service.ts, backend/apps/cloud/src/analytics/entities/salt.entity.ts, backend/apps/community/src/analytics/entities/salt.entity.ts
Introduced SaltService for rotation-based salt management with Redis caching and MySQL persistence. Added Salt entity to store salts by rotation (daily/monthly). Implements getGlobalSalt, getSaltForSession, getSaltForProfile with automatic expiration and regeneration.
Analytics Service Updates
backend/apps/cloud/src/analytics/analytics.service.ts, backend/apps/community/src/analytics/analytics.service.ts
Replaced Redis sessionHash with salted PSID derivation. Added methods: derivePsidFromInputs, getSessionKey, extendSessionTTL, recordSession, updateSessionActivity. Introduced profile prefix constants (PROFILE_PREFIX_ANON, PROFILE_PREFIX_USER) and profile-related queries (getProfilesList, getProfileDetails, getProfileTopPages, getProfileActivityCalendar, getProfileChartData, getProfileSessionsList). Migrated session duration calculations from session_durations table to ClickHouse sessions table with dateDiff.
Controller Endpoints
backend/apps/cloud/src/analytics/analytics.controller.ts
Added three new public endpoints: getProfiles, getProfile, getProfileSessions with corresponding DTOs for profile-scoped analytics. Integrated profile ID generation and session recording across heartbeat, log, custom events, noscript, and error logging flows.
Profile & Event DTOs
backend/apps/cloud/src/analytics/dto/get-profiles.dto.ts, backend/apps/cloud/src/analytics/dto/get-profile.dto.ts, backend/apps/cloud/src/analytics/dto/pageviews.dto.ts, backend/apps/cloud/src/analytics/dto/events.dto.ts, backend/apps/cloud/src/analytics/dto/error.dto.ts
Added GetProfilesDto, GetProfileDto, GetProfileSessionsDto. Extended PageviewsDto, EventsDto, ErrorDto with optional profileId field for user-supplied or auto-generated profile tracking.
Analytics Module Configuration
backend/apps/cloud/src/analytics/analytics.module.ts, backend/apps/community/src/analytics/analytics.module.ts
Added TypeOrmModule for Salt entity and SaltService to providers/exports in AnalyticsModule.
Task Manager & Cron Jobs
backend/apps/cloud/src/task-manager/task-manager.service.ts, backend/apps/community/src/task-manager/task-manager.service.ts, backend/apps/community/src/task-manager/task-manager.module.ts
Replaced generateSessionSalt and processSessionDuration cron jobs with hourly regenerateGlobalSalts delegating to SaltService. Added SaltService injection. Updated module imports to include forwardRef(AnalyticsModule) in community app.
Constants & Utilities
backend/apps/cloud/src/common/constants.ts, backend/apps/community/src/common/constants.ts, backend/apps/cloud/src/analytics/utils/transformers.ts, backend/apps/community/src/common/utils.ts
Removed REDIS_SESSION_SALT_KEY and countKeysByPattern. Added profileId parameter to trafficTransformer, customEventTransformer, errorEventTransformer. Added saltRotation to ALLOWED_KEYS for project updates.
Project Entity Schema
backend/apps/cloud/src/project/entity/project.entity.ts, backend/apps/community/src/project/entity/project.entity.ts
Added SaltRotation enum (DAILY, WEEKLY, MONTHLY) and saltRotation property to Project entity.
ClickHouse Migrations
backend/migrations/clickhouse/2025_12_02_profile_id.js, backend/migrations/clickhouse/selfhosted_2025_12_02_profile_id.js, backend/migrations/clickhouse/initialise_database.js
Added profileId (Nullable String) to analytics, customEV, errors tables. Created new sessions table with ReplacingMergeTree engine, partitioned by firstSeen, ordered by (pid, psid). Migrated historical data from session_durations to sessions with computed firstSeen and lastSeen.
MySQL Migrations
backend/migrations/mysql/2025_11_30_salt_rotation.sql, backend/migrations/mysql/2025_12_05_salt_table.sql, backend/migrations/clickhouse/initialise_selfhosted.js, backend/migrations/clickhouse/selfhosted_2025_11_30_salt_rotation.js
Added saltRotation ENUM column to project table (default 'daily'). Created salt table with rotation PK, salt TEXT, expiresAt, created timestamps.
Frontend API Layer
web/app/api/index.ts
Added three new API functions: getProfiles, getProfile, getProfileSessions for fetching profile data with period, filters, pagination, and timezone support.
Frontend Models & Constants
web/app/lib/models/Project.ts, web/app/lib/constants/index.ts
Added Profile and ProfileDetails interfaces with session counts, device info, charts, activity calendars. Extended Project interface with saltRotation field. Added profiles tab to PRODUCTION_PROJECT_TABS and users tab to SELFHOSTED_PROJECT_TABS.
Frontend UI Components
web/app/pages/Project/View/components/Users.tsx, web/app/pages/Project/View/components/UserDetails.tsx, web/app/utils/profileAvatars.tsx, web/app/ui/Tooltip.tsx
Introduced Users list component with filtering by profile type (all/anonymous/identified). Added UserDetails component for comprehensive profile view with stats, activity calendar, pageviews chart, and sessions. Created profileAvatars utility with deterministic avatar generation and display name formatting. Added configurable delay prop to Tooltip.
Frontend Views & Shortcuts
web/app/pages/Project/View/ViewProject.tsx, web/app/pages/Project/View/ViewProject.helpers.tsx, web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/pages/Project/Settings/tabs/General.tsx, web/app/pages/Project/View/components/RefreshStatsButton.tsx
Added profiles tab to Project View with full profile management flow (loading, filtering, pagination, session details). Integrated profile API calls with state management. Added keyboard shortcut Shift+U for profiles tab. Extended General settings to include saltRotation dropdown selector with daily/weekly/monthly options. Updated styling in RefreshStatsButton.
Localization & Dependencies
web/public/locales/en.json, web/package.json
Added translation keys for profiles UI (backToUsers, allUsers, anonymous, identified, searchUsers, sessions, avgDuration, etc.) and saltRotation configuration block. Added "boring-avatars" npm dependency (^2.0.4) for avatar generation.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Controller as AnalyticsController
    participant Service as AnalyticsService
    participant SaltService
    participant Redis
    participant MySQL as MySQL/DB
    participant ClickHouse
    
    autonumber
    
    Client->>Controller: POST /log (with pageview/event)
    activate Controller
    
    Controller->>Service: getSessionId(pid, userAgent, ip)
    activate Service
    
    Service->>SaltService: getSaltForSession()
    activate SaltService
    SaltService->>Redis: GET salt:daily
    alt Cache Hit
        Redis-->>SaltService: cached salt
    else Cache Miss
        SaltService->>MySQL: SELECT FROM salt WHERE rotation='daily'
        alt Valid Salt Exists
            MySQL-->>SaltService: existing salt
            SaltService->>Redis: SET salt:daily (TTL 10min)
        else Generate New
            SaltService->>SaltService: generateSalt(bcrypt)
            SaltService->>MySQL: UPSERT INTO salt
            SaltService->>Redis: SET salt:daily (TTL 10min)
        end
    end
    SaltService-->>Service: salt value
    deactivate SaltService
    
    Service->>Service: derivePsidFromInputs(pid, userAgent, ip, salt)
    Service->>Service: getSessionKey(psid)
    Service->>Redis: EXISTS ses:psid
    alt Session Exists
        Redis-->>Service: true
    else New Session
        Redis-->>Service: false
        Service->>ClickHouse: INSERT INTO sessions(psid, pid, profileId, firstSeen, lastSeen, pageviews)
    end
    Service-->>Controller: { exists, psid }
    deactivate Service
    
    Controller->>Service: recordSession(psid, pid, profileId, isPageview)
    activate Service
    Service->>ClickHouse: UPDATE sessions (increment pageviews/events, update lastSeen)
    Service->>Service: transformers apply profileId
    ClickHouse-->>Service: ✓
    Service-->>Controller: ✓
    deactivate Service
    
    Controller->>Service: extendSessionTTL(psid)
    activate Service
    Service->>Redis: EXPIRE ses:psid 30min
    Redis-->>Service: ✓
    Service-->>Controller: ✓
    deactivate Service
    
    Controller-->>Client: 200 OK
    deactivate Controller
Loading
sequenceDiagram
    participant User as Frontend User
    participant UI as React Component
    participant API as API Layer
    participant Controller as AnalyticsController
    participant Service as AnalyticsService
    participant ClickHouse
    
    autonumber
    
    User->>UI: Select Profiles Tab
    activate UI
    
    UI->>API: getProfiles(pid, period, filters, take, skip, timezone)
    activate API
    API->>Controller: GET /profiles
    activate Controller
    
    Controller->>Service: getProfilesList(pid, filtersQuery, timezone, take, skip, profileType)
    activate Service
    Service->>ClickHouse: SELECT profileId, COUNT sessions, COUNT pageviews FROM sessions WHERE pid=? GROUP BY profileId LIMIT take OFFSET skip
    ClickHouse-->>Service: profiles with metrics
    Service-->>Controller: { profiles, appliedFilters, take, skip }
    deactivate Service
    
    Controller-->>API: 200 + profile data
    deactivate Controller
    API-->>UI: profiles array
    deactivate API
    
    UI->>UI: Render profile list
    User->>UI: Click on profile row
    
    UI->>API: getProfile(pid, profileId, period, timezone)
    activate API
    API->>Controller: GET /profiles/:profileId
    activate Controller
    
    Controller->>Service: getProfileDetails(pid, profileId, timezone)
    activate Service
    Service->>ClickHouse: SELECT details, avgDuration FROM sessions/analytics WHERE profileId=?
    ClickHouse-->>Service: profile details
    Service-->>Controller: ProfileDetails
    deactivate Service
    
    Controller->>Service: getProfileTopPages(pid, profileId)
    Service->>ClickHouse: SELECT page, COUNT FROM analytics WHERE profileId=? GROUP BY page
    ClickHouse-->>Service: top pages
    
    Controller->>Service: getProfileActivityCalendar(pid, profileId, months)
    Service->>ClickHouse: SELECT DATE(timestamp), COUNT FROM analytics WHERE profileId=? GROUP BY DATE(timestamp)
    ClickHouse-->>Service: activity calendar
    
    Controller->>Service: getProfileChartData(pid, profileId, timeBucket, from, to, timezone)
    Service->>ClickHouse: SELECT timeBucket, pageviews, customEvents, errors FROM analytics WHERE profileId=? AND timestamp BETWEEN from AND to
    ClickHouse-->>Service: chart data
    
    Controller-->>API: { details, topPages, activityCalendar, chart }
    deactivate Controller
    API-->>UI: combined profile details
    deactivate API
    
    UI->>UI: Render UserDetails component with charts, stats, sessions
    UI->>User: Display complete profile
    deactivate UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • PSID Derivation Logic: Verify salted hash computation across derivePsidFromInputs in both cloud and community services; ensure deterministic and collision-free results
  • Session Recording Migration: Confirm all event flows (log, custom events, heartbeat, noscript, errors) correctly call recordSession and updateSessionActivity with proper profileId
  • ClickHouse Schema Changes: Review migration scripts for data correctness, especially historical data migration from session_durations to sessions table with computed firstSeen/lastSeen
  • Redis TTL Management: Verify session TTL extension via extendSessionTTL with proper Redis key management (ses:psid format)
  • Profile Queries: Validate ClickHouse aggregations for profile lists, details, top pages, and activity calendars; ensure timezone handling is consistent
  • Frontend Profile State: Check state management in ViewProject.tsx for profile loading, pagination, and filtering logic
  • Database Constraints: Verify saltRotation enum consistency across MySQL and ClickHouse schemas and Project entity
  • Transaction Safety: Review module-level imports in task-manager and circular dependency handling with forwardRef

Poem

🐰 Salted sessions now flow through channels deep,
Profiles emerge where users' journeys keep,
ClickHouse stores the tales of mice and tracks,
No more Redis hashing—just salt in our sacks!
Avatar bunnies greet each visitor fair, 🎨🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Profiles / MAU' is concise but vague and generic, using abbreviated terminology without clear context about the primary changes. Expand the title to be more descriptive and specific, e.g., 'Add Profiles feature with salt-derived session IDs' to clarify the main changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The PR description addresses the template requirements with the main changes stated (Profiles, salt-derived session IDs) and both database migrations and community edition support confirmed, though documentation updates remain incomplete.
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 changed the title Make tracking salt adjustable (to support MAU metric) Profiles / MAU Dec 3, 2025
@Blaumaus Blaumaus changed the base branch from main to swetrix-revamp December 5, 2025 00:13
@Blaumaus Blaumaus marked this pull request as ready for review December 5, 2025 01:53
@Blaumaus
Copy link
Member Author

Blaumaus commented Dec 5, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

✅ 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: 15

Caution

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

⚠️ Outside diff range comments (1)
web/app/lib/constants/index.ts (1)

471-492: Fix SELFHOSTED_PROJECT_TABS key mismatch: users should be profiles

At line 474, SELFHOSTED_PROJECT_TABS defines the key as users, but code throughout the codebase (e.g., line 1472 in ViewProject.tsx) accesses PROJECT_TABS.profiles. This causes the profiles tab to have an undefined id on self-hosted deployments.

Change line 474 from users: 'users' to profiles: 'profiles' to align with production and all existing usages, or ensure all code branches on isSelfhosted before accessing environment-specific keys.

🧹 Nitpick comments (20)
web/app/ui/Tooltip.tsx (1)

36-40: LGTM! Configurable delay enhances flexibility.

The addition of the delay prop with a default value of 200 preserves backward compatibility while enabling callers to customize the tooltip delay. The implementation correctly forwards the delay to TooltipProvider.

Optional observation: The prop name delay shares its name with a utility function in web/app/utils/generic.ts (line 147). While there's no actual collision since the utility isn't imported here, consider whether a more specific name like tooltipDelay might reduce potential confusion for future maintainers.

backend/apps/cloud/src/analytics/dto/pageviews.dto.ts (1)

31-37: Optional profileId on PageviewsDto is consistent with the profiles model

The new field and its Swagger/validator annotations look good; if there are any backend constraints (length, charset) on profileId, consider documenting them here to keep clients aligned with storage/query expectations.

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

6-26: SaltRotation enum + Project.saltRotation property align with the new rotation feature

The enum values and comments clearly document DAU/WAU/MAU semantics, and adding saltRotation: SaltRotation on Project matches the new schema/defaults. If you expect to touch partially migrated data anywhere, consider making saltRotation optional in the TS type until all environments are fully upgraded.

backend/apps/cloud/src/analytics/dto/events.dto.ts (1)

90-97: profileId on EventsDto matches the rest of the profile-aware DTOs

This optional field is wired consistently with the pageviews/error DTOs and clearly documented for API consumers; if there are backend limits on profileId (length/charset), adding them to the description or via validators would make client behavior more predictable.

backend/apps/cloud/src/analytics/entities/salt.entity.ts (1)

13-14: Consider adding an index on expiresAt for cleanup queries.

If there will be scheduled cleanup of expired salts, an index on expiresAt would improve query performance. This is optional if the table remains small (one row per rotation type).

backend/apps/cloud/src/analytics/dto/error.dto.ts (1)

35-42: LGTM! Consider adding @MaxLength validation for consistency.

The profileId field is properly decorated. For consistency with other string fields in this DTO and to prevent excessively long profile IDs from being stored, consider adding a @MaxLength constraint.

  @ApiProperty({
    example: 'user_12345',
    description:
      'Optional profile ID for long-term user tracking. If not provided, one will be auto-generated.',
  })
  @IsOptional()
  @IsString()
+ @MaxLength(128)
  profileId?: string
backend/migrations/clickhouse/initialise_database.js (1)

151-164: LGTM! Well-designed sessions table with ReplacingMergeTree.

The ReplacingMergeTree(lastSeen) engine is appropriate for session tracking — it automatically deduplicates rows with the same (pid, psid) key, keeping the row with the latest lastSeen timestamp. The partition by firstSeen month enables efficient data lifecycle management.

Consider adding a TTL clause if sessions should be automatically cleaned up after a retention period:

TTL firstSeen + INTERVAL 2 YEAR
web/app/pages/Project/Settings/ProjectSettings.tsx (1)

800-806: Type safety concern: casting to any loses type checking.

The saltRotation: name as any cast bypasses TypeScript's type checking. Consider using a proper type assertion or defining the allowed values explicitly.

-                    setSaltRotation={(name) =>
-                      setForm((prevForm) => ({
-                        ...prevForm,
-                        saltRotation: name as any,
-                      }))
-                    }
+                    setSaltRotation={(name: 'daily' | 'weekly' | 'monthly') =>
+                      setForm((prevForm) => ({
+                        ...prevForm,
+                        saltRotation: name,
+                      }))
+                    }
web/app/pages/Project/View/ViewProject.helpers.tsx (1)

1666-1677: Consider removing unused shift+e listener.

The _SHORTCUTS_TABS_LISTENERS includes shift+e, but SHORTCUTS_TABS_MAP doesn't have an 'E' key mapping. If 'E' was previously used for something that's now removed, consider cleaning up the listener to avoid confusion.

-const _SHORTCUTS_TABS_LISTENERS = 'shift+t, shift+p, shift+u, shift+s, shift+f, shift+e, shift+r'
+const _SHORTCUTS_TABS_LISTENERS = 'shift+t, shift+p, shift+u, shift+s, shift+f, shift+r'
web/app/pages/Project/Settings/tabs/General.tsx (1)

70-73: Consider fixing the type mismatch instead of suppressing.

The @ts-expect-error and item: any in labelExtractor indicate a type mismatch between saltRotationOptions and what Select expects. If the Select component's items prop expects a different shape, consider either:

  1. Updating the saltRotationOptions type to match the expected interface
  2. Adding proper generics to the Select component

This would improve type safety and IDE support.

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

14-17: Consider adding error handling and logging.

The cron job delegates to saltService.regenerateExpiredSalts() but doesn't handle potential errors. If the salt regeneration fails, there's no logging or alerting.

 @Cron(CronExpression.EVERY_HOUR)
 async regenerateGlobalSalts() {
-  await this.saltService.regenerateExpiredSalts()
+  try {
+    await this.saltService.regenerateExpiredSalts()
+  } catch (error) {
+    this.logger.error(`[CRON WORKER](regenerateGlobalSalts) Error occurred: ${error}`)
+  }
 }
web/app/utils/profileAvatars.tsx (1)

190-199: Consider handling empty or malformed profileId.

If profileId is an empty string, generateDisplayName will always return the same name (first adjective + first noun). You may want to add a guard:

 export const getProfileDisplayName = (profileId: string, isIdentified: boolean): string => {
+  if (!profileId) {
+    return 'Unknown User'
+  }
   if (isIdentified) {
backend/apps/cloud/src/task-manager/task-manager.service.ts (1)

689-692: Add error handling consistent with other cron jobs in this file.

Other cron jobs in this service use Promise.allSettled(...).catch() or try/catch blocks with logging. This job should follow the same pattern:

 @Cron(CronExpression.EVERY_HOUR)
 async regenerateGlobalSalts() {
-  await this.saltService.regenerateExpiredSalts()
+  try {
+    await this.saltService.regenerateExpiredSalts()
+  } catch (reason) {
+    this.logger.error(
+      `[CRON WORKER](regenerateGlobalSalts) Error occured: ${reason}`,
+    )
+  }
 }
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1)

37-41: Add @IsOptional() decorators to from and to fields for consistency.

These fields are typed as optional with ? but lack the @IsOptional() decorator that other optional fields in this DTO use (e.g., timezone, period). This inconsistency could lead to validation failures.

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

   @ApiProperty({ required: false })
+  @IsOptional()
   to?: string
backend/apps/cloud/src/analytics/salt.service.ts (1)

44-46: Consider using a higher bcrypt cost factor for production salts.

While cost factor 10 is reasonable for development, production security-sensitive salts may benefit from a higher factor (12-14). However, since these salts are cached and regenerated infrequently, the performance impact would be minimal.

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

3908-3941: Ensure UserDetails safely handles details={activeProfile} when it can be null

In the profile detail render, UserDetails is invoked with details={activeProfile}, but activeProfile is explicitly set to null on load failures and before initial data arrives. Make sure UserDetails’s prop typing and implementation allow details to be null (or gate the render on activeProfile being truthy) to avoid runtime errors.

For example, either:

  • Type details as ProfileDetails | null and handle the empty state inside UserDetails, or
  • Wrap the component: {activeProfile && <UserDetails details={activeProfile} .../>}.
web/app/pages/Project/View/components/Users.tsx (1)

37-137: Profiles list & filter components look solid; consider a small a11y tweak

The UserRow, Users, and UsersFilter components are well structured and integrate cleanly with the new profiles flow (URL state, time formatting, badges, icons).

One minor accessibility improvement: since UsersFilter’s three buttons behave as toggleable filter pills, you could mark them as toggle buttons with aria-pressed so screen readers convey their state:

-      <button
+      <button
         type='button'
         onClick={() => onProfileTypeChange('all')}
-        className={filterButtonClass(profileType === 'all')}
+        className={filterButtonClass(profileType === 'all')}
+        aria-pressed={profileType === 'all'}
       >
         {t('project.allUsers')}
       </button>
...
-      <button
+      <button
         type='button'
         onClick={() => onProfileTypeChange('anonymous')}
-        className={filterButtonClass(profileType === 'anonymous')}
+        className={filterButtonClass(profileType === 'anonymous')}
+        aria-pressed={profileType === 'anonymous'}
       >
         {t('project.anonymous')}
       </button>
...
-      <button
+      <button
         type='button'
         onClick={() => onProfileTypeChange('identified')}
-        className={filterButtonClass(profileType === 'identified')}
+        className={filterButtonClass(profileType === 'identified')}
+        aria-pressed={profileType === 'identified'}
       >
         {t('project.identified')}
       </button>

Purely optional, but improves assistive-tech behavior without changing visuals.

Also applies to: 140-181, 183-201

backend/apps/community/src/analytics/analytics.service.ts (1)

1124-1137: Consider using delimiters in hash input to prevent potential concatenation collisions.

While unlikely in practice, concatenating strings without delimiters could theoretically lead to collisions (e.g., "abc" + "def" equals "ab" + "cdef"). Adding a delimiter would be more robust.

 derivePsidFromInputs(
   pid: string,
   userAgent: string,
   ip: string,
   salt: string,
 ): string {
-  const combined = `${userAgent}${ip}${pid}${salt}`
+  const combined = `${userAgent}|${ip}|${pid}|${salt}`
   return crypto
     .createHash('sha256')
     .update(combined)
     .digest()
     .readBigUInt64BE(0)
     .toString()
 }
backend/apps/cloud/src/analytics/analytics.service.ts (2)

1682-1689: Session‑duration aggregation doesn’t fully honor filters/time windows and may overweight noisy sessions

A few details around sdur could surprise you:

  • In the period === 'all' path, duration_avg is avg(dateDiff('second', firstSeen, lastSeen)) FROM sessions FINAL WHERE pid = {pid}, i.e. it ignores filtersQuery. The counts (all/unique) are filtered; the duration is global for the project.
  • In the non‑all path, you constrain sessions by psid IN (SELECT DISTINCT psid FROM analytics ... BETWEEN groupFromUTC/groupToUTC ${filtersQuery}) – good – but:
    • generateAnalyticsAggregationQuery joins sessions_data on (pid, psid) without deduping the subquery. Sessions with more pageviews in a bucket contribute their duration multiple times, biasing the bucket’s average toward noisy sessions.
    • sessions_data itself is not time‑bounded, so a long‑lived session’s full duration is reused for every bucket where it has at least one event.

If you want sdur to mean “average session duration for the same filtered/time‑bounded sessions”:

  • Consider constraining sessions in the period === 'all' path via the same psid set as analytics (or computing durations from analytics directly, as you already do in getProfileDetails).
  • In generateAnalyticsAggregationQuery, change the inner analytics subquery to dedupe by psid per bucket (e.g. SELECT DISTINCT pid, psid, tz_created ...) before joining to sessions_data, so each session contributes once per bucket.

These aren’t hard correctness bugs, but they do make the reported sdur harder to interpret.

Also applies to: 1764-1778, 1794-1808, 2555-2582


4310-4401: Profile queries work but have mixed time/filter semantics and a couple of minor nits

A few things to be aware of in the new profile APIs:

  • Time window in getProfilesList: the CTEs over analytics, customEV, and errors don’t constrain created by {groupFrom} / {groupTo} at all – only ${filtersQuery} is applied (and only to analytics). If the UI expects “profiles active in the selected period”, this will currently return lifetime stats. If that’s not intentional, add a created BETWEEN {groupFrom:String} AND {groupTo:String} clause consistent with the rest of the service.
  • Filter symmetry: filtersQuery is only applied in the analytics CTE of getProfilesList / getProfileSessionsList, not in the customEV / errors CTEs. That means pageviews respect filters while events/errors do not. If filters are meant to narrow “per‑profile events/errors” the same way, consider threading ${filtersQuery} into those CTEs as well.
  • Session duration for profile sessions: session_duration_agg in getProfileSessionsList reuses the avg(dateDiff('second', firstSeen, lastSeen)) pattern from the global sessions list, without time bounds. This shares the same concerns as the main sessions duration comment – once sessions store multiple rows, sdur is unlikely to reflect real session lengths.
  • Unused timezone args: getProfilesList accepts safeTimezone but doesn’t use it, and getProfileDetails prefixes it with _ to silence TS. If you don’t plan to timezone‑shift profile timestamps server‑side, you can drop these parameters to keep the signature lean.

Functionally everything should run, but aligning time windows/filters across analytics, events, errors, and sessions will make the profile views much more predictable.

Also applies to: 4403-4515, 4517-4542, 4544-4575, 4577-4681, 4683-4787

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ebb644 and ff45d0a.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (16 hunks)
  • backend/apps/cloud/src/analytics/analytics.module.ts (1 hunks)
  • backend/apps/cloud/src/analytics/analytics.service.ts (12 hunks)
  • backend/apps/cloud/src/analytics/dto/error.dto.ts (1 hunks)
  • backend/apps/cloud/src/analytics/dto/events.dto.ts (1 hunks)
  • backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1 hunks)
  • backend/apps/cloud/src/analytics/dto/get-profiles.dto.ts (1 hunks)
  • backend/apps/cloud/src/analytics/dto/pageviews.dto.ts (1 hunks)
  • backend/apps/cloud/src/analytics/entities/salt.entity.ts (1 hunks)
  • backend/apps/cloud/src/analytics/salt.service.ts (1 hunks)
  • backend/apps/cloud/src/analytics/utils/transformers.ts (6 hunks)
  • backend/apps/cloud/src/common/constants.ts (0 hunks)
  • backend/apps/cloud/src/project/entity/project.entity.ts (2 hunks)
  • backend/apps/cloud/src/task-manager/task-manager.service.ts (3 hunks)
  • backend/apps/community/src/analytics/analytics.controller.ts (11 hunks)
  • backend/apps/community/src/analytics/analytics.module.ts (1 hunks)
  • backend/apps/community/src/analytics/analytics.service.ts (11 hunks)
  • backend/apps/community/src/analytics/entities/salt.entity.ts (1 hunks)
  • backend/apps/community/src/analytics/salt.service.ts (1 hunks)
  • backend/apps/community/src/common/constants.ts (0 hunks)
  • backend/apps/community/src/common/utils.ts (1 hunks)
  • backend/apps/community/src/project/entity/project.entity.ts (2 hunks)
  • backend/apps/community/src/task-manager/task-manager.module.ts (1 hunks)
  • backend/apps/community/src/task-manager/task-manager.service.ts (1 hunks)
  • backend/migrations/clickhouse/2025_12_02_profile_id.js (1 hunks)
  • backend/migrations/clickhouse/initialise_database.js (4 hunks)
  • backend/migrations/clickhouse/initialise_selfhosted.js (1 hunks)
  • backend/migrations/clickhouse/selfhosted_2025_11_30_salt_rotation.js (1 hunks)
  • backend/migrations/clickhouse/selfhosted_2025_12_02_profile_id.js (1 hunks)
  • backend/migrations/mysql/2025_11_30_salt_rotation.sql (1 hunks)
  • backend/migrations/mysql/2025_12_05_salt_table.sql (1 hunks)
  • web/app/api/index.ts (2 hunks)
  • web/app/lib/constants/index.ts (2 hunks)
  • web/app/lib/models/Project.ts (2 hunks)
  • web/app/pages/Project/Settings/ProjectSettings.tsx (3 hunks)
  • web/app/pages/Project/Settings/tabs/General.tsx (2 hunks)
  • web/app/pages/Project/View/ViewProject.helpers.tsx (1 hunks)
  • web/app/pages/Project/View/ViewProject.tsx (10 hunks)
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx (1 hunks)
  • web/app/pages/Project/View/components/UserDetails.tsx (1 hunks)
  • web/app/pages/Project/View/components/Users.tsx (1 hunks)
  • web/app/ui/Tooltip.tsx (1 hunks)
  • web/app/utils/profileAvatars.tsx (1 hunks)
  • web/package.json (1 hunks)
  • web/public/locales/en.json (3 hunks)
💤 Files with no reviewable changes (2)
  • backend/apps/community/src/common/constants.ts
  • backend/apps/cloud/src/common/constants.ts
🧰 Additional context used
🧬 Code graph analysis (18)
backend/migrations/clickhouse/selfhosted_2025_11_30_salt_rotation.js (1)
backend/migrations/clickhouse/setup.js (2)
  • dbName (107-107)
  • queriesRunner (76-105)
web/app/ui/Tooltip.tsx (1)
web/app/utils/generic.ts (1)
  • delay (148-148)
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (1)
web/app/lib/constants/index.ts (1)
  • DEFAULT_TIMEZONE (349-349)
backend/apps/community/src/analytics/entities/salt.entity.ts (1)
backend/apps/community/src/analytics/salt.service.ts (1)
  • SaltRotationType (17-17)
backend/apps/community/src/analytics/analytics.module.ts (2)
backend/apps/cloud/src/analytics/analytics.module.ts (1)
  • Module (12-23)
backend/apps/community/src/task-manager/task-manager.module.ts (1)
  • Module (6-11)
backend/apps/cloud/src/analytics/analytics.module.ts (2)
backend/apps/community/src/analytics/analytics.module.ts (1)
  • Module (11-17)
backend/apps/community/src/task-manager/task-manager.module.ts (1)
  • Module (6-11)
backend/apps/cloud/src/analytics/entities/salt.entity.ts (1)
backend/apps/cloud/src/analytics/salt.service.ts (1)
  • SaltRotationType (17-17)
backend/migrations/clickhouse/initialise_database.js (1)
backend/migrations/clickhouse/setup.js (1)
  • dbName (107-107)
web/app/pages/Project/View/components/UserDetails.tsx (7)
web/app/lib/models/Project.ts (1)
  • Session (85-99)
web/app/utils/generic.ts (4)
  • cn (150-152)
  • getStringFromTime (81-91)
  • getTimeFromSeconds (64-79)
  • getLocaleDisplayName (134-146)
web/app/lib/constants/index.ts (3)
  • BROWSER_LOGO_MAP (930-977)
  • OS_LOGO_MAP (979-1005)
  • OS_LOGO_MAP_DARK (1007-1012)
web/app/providers/ThemeProvider.tsx (1)
  • useTheme (29-37)
web/app/utils/profileAvatars.tsx (2)
  • getProfileDisplayName (190-199)
  • ProfileAvatar (210-216)
web/app/pages/Project/View/components/SessionChart.tsx (1)
  • SessionChart (24-52)
web/app/pages/Project/View/components/Sessions.tsx (1)
  • Sessions (165-183)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (490-492)
backend/apps/cloud/src/analytics/salt.service.ts (3)
backend/apps/community/src/analytics/salt.service.ts (1)
  • SaltRotationType (17-17)
backend/apps/community/src/common/constants.ts (1)
  • redis (105-105)
backend/apps/cloud/src/common/constants.ts (1)
  • redis (170-170)
web/app/api/index.ts (2)
web/app/lib/models/Project.ts (3)
  • Profile (122-135)
  • ProfileDetails (137-153)
  • Session (85-99)
web/app/pages/Project/View/interfaces/traffic.ts (1)
  • Filter (3-9)
web/app/pages/Project/View/components/Users.tsx (2)
web/app/utils/profileAvatars.tsx (2)
  • getProfileDisplayName (190-199)
  • ProfileAvatar (210-216)
web/app/ui/Badge.tsx (1)
  • Badge (11-34)
backend/apps/cloud/src/analytics/analytics.controller.ts (4)
backend/apps/cloud/src/analytics/utils/transformers.ts (1)
  • customEventTransformer (77-128)
backend/apps/community/src/analytics/utils/transformers.ts (1)
  • customEventTransformer (75-124)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getLowestPossibleTimeBucket (205-246)
backend/apps/cloud/src/analytics/dto/get-profile.dto.ts (2)
  • GetProfileDto (8-42)
  • GetProfileSessionsDto (44-66)
backend/apps/community/src/analytics/analytics.service.ts (2)
backend/apps/community/src/common/constants.ts (2)
  • redis (105-105)
  • UNIQUE_SESSION_LIFE_TIME (109-109)
backend/apps/cloud/src/common/constants.ts (2)
  • redis (170-170)
  • UNIQUE_SESSION_LIFE_TIME (174-174)
web/app/pages/Project/View/ViewProject.tsx (5)
web/app/lib/models/Project.ts (3)
  • Profile (122-135)
  • ProfileDetails (137-153)
  • Session (85-99)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (490-492)
web/app/api/index.ts (3)
  • getProfiles (669-695)
  • getProfile (697-718)
  • getProfileSessions (720-746)
web/app/pages/Project/View/components/Users.tsx (2)
  • UsersFilter (145-181)
  • Users (183-201)
web/app/pages/Project/View/components/UserDetails.tsx (1)
  • UserDetails (195-402)
backend/apps/cloud/src/analytics/analytics.service.ts (3)
backend/apps/cloud/src/common/constants.ts (2)
  • redis (170-170)
  • UNIQUE_SESSION_LIFE_TIME (174-174)
backend/apps/community/src/common/utils.ts (1)
  • hash (79-85)
backend/apps/cloud/src/common/utils.ts (1)
  • hash (22-28)
backend/apps/community/src/task-manager/task-manager.module.ts (2)
backend/apps/community/src/analytics/analytics.module.ts (1)
  • Module (11-17)
backend/apps/cloud/src/analytics/analytics.module.ts (1)
  • Module (12-23)
🪛 Biome (2.1.2)
web/public/locales/en.json

[error] 1029-1029: The key noData was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

🔇 Additional comments (51)
web/app/pages/Project/View/components/RefreshStatsButton.tsx (1)

103-103: LGTM! Z-index addition fixes tooltip overlap.

Adding z-30 ensures the tooltip appears above other elements, resolving potential stacking issues. The value is appropriate for tooltip layering.

web/package.json (1)

39-39: New boring-avatars dependency looks appropriate

The dependency addition is straightforward and scoped; just ensure the lockfile is updated and CI/build output looks good (bundle size, SSR compatibility) once the new avatar UI is wired.

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

115-128: Allowing saltRotation updates in ClickHouse utilities is correct

Adding 'saltRotation' to ALLOWED_KEYS (and therefore to CLICKHOUSE_PROJECT_UPDATABLE_KEYS) cleanly enables updating the rotation mode while keeping the existing filtering logic intact and type handling (INT8_COLUMNS) unchanged.

backend/apps/cloud/src/analytics/utils/transformers.ts (1)

26-181: Extending transformers with profileId is consistent but tightens call-site requirements

Adding profileId as a required argument and field in the traffic/custom/error transformers cleanly aligns stored rows with the new profile dimension. Please double‑check that all call sites now pass a normalized, non‑PII profileId and that the corresponding ClickHouse tables include a matching profileId column with compatible type.

backend/migrations/clickhouse/initialise_selfhosted.js (1)

21-36: Adding saltRotation to the self‑hosted ClickHouse project table is aligned with the entity

The new saltRotation String DEFAULT 'daily' column matches the Project entity and rotation model; ensure the standalone migrations for existing self‑hosted installations add the same column/default so upgrade paths stay consistent.

backend/migrations/clickhouse/selfhosted_2025_11_30_salt_rotation.js (1)

1-8: LGTM! Migration follows established patterns.

The migration correctly adds the saltRotation column with a sensible default. Note that ClickHouse uses String type while the MySQL migration uses ENUM('daily', 'weekly', 'monthly') — this is acceptable since ClickHouse doesn't have a native enum equivalent for this use case, but application-level validation will be the enforcement point.

backend/migrations/mysql/2025_11_30_salt_rotation.sql (1)

1-3: LGTM! Migration is well-documented.

The migration correctly adds the saltRotation column with appropriate ENUM values matching the SaltRotation enum in project.entity.ts. The comment clearly explains the column's purpose.

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

27-31: LGTM! Well-documented enum with clear use cases.

The SaltRotation enum is properly defined with helpful comments explaining each rotation period's purpose (DAU, WAU, MAU tracking).


99-105: LGTM! Column properly configured.

The saltRotation property is correctly decorated with TypeORM and Swagger decorators, with an appropriate default value.

backend/migrations/mysql/2025_12_05_salt_table.sql (1)

1-9: LGTM! Schema aligns with the Salt entity.

The table structure correctly matches the Salt TypeORM entity. The primary key on rotation ensures only one active salt per rotation type, which is the intended design.

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

1-21: LGTM! Module properly integrates Salt infrastructure.

The AnalyticsModule correctly registers the Salt entity with TypeORM and provides/exports SaltService. This follows the same pattern as the community module.

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

10-11: LGTM! profileId column added consistently.

The profileId column is correctly added as Nullable(String) since not all events will have an associated profile.

backend/apps/community/src/analytics/analytics.module.ts (1)

1-17: LGTM! Module configuration is correct and consistent with the cloud edition.

The module properly registers the Salt entity with TypeORM, provides SaltService, and exports it for dependent modules. The structure aligns with the cloud edition's analytics module.

backend/apps/community/src/task-manager/task-manager.module.ts (1)

1-11: LGTM! Proper use of forwardRef for circular dependency resolution.

The forwardRef(() => AnalyticsModule) correctly handles the circular dependency between TaskManagerModule and AnalyticsModule, enabling TaskManagerService to access SaltService for salt regeneration workflows.

backend/apps/community/src/analytics/entities/salt.entity.ts (1)

1-18: LGTM! Well-structured entity for salt persistence.

The entity design is appropriate:

  • Primary key on rotation ensures one salt per rotation type
  • expiresAt enables expiration-based rotation logic
  • Timestamp columns use proper database defaults
backend/migrations/clickhouse/2025_12_02_profile_id.js (2)

29-42: Migration semantics note: lastSeen for historical data reflects migration time.

The migration correctly computes firstSeen from the duration, but lastSeen is set to now() (migration execution time) for all historical records. This is acceptable since the original session_durations table didn't track exact timestamps, but be aware that historical session data will show the migration date as lastSeen.


44-45: Good practice: commented-out DROP TABLE for manual verification.

The commented-out DROP TABLE allows manual verification of migrated data before removing the source table. Consider adding a follow-up migration or documentation note to remind operators to drop the table after verification.

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

122-153: LGTM! Well-structured Profile interfaces.

The Profile and ProfileDetails interfaces provide comprehensive typing for profile data with appropriate optional fields. The extension pattern (ProfileDetails extends Profile) is clean and maintainable.

backend/migrations/clickhouse/selfhosted_2025_12_02_profile_id.js (1)

13-27: Sessions table schema looks well-designed.

The ReplacingMergeTree(lastSeen) engine with ORDER BY (pid, psid) correctly enables upsert semantics for session updates. Partitioning by toYYYYMM(firstSeen) is appropriate for time-series data.

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

65-78: Salt rotation selector implementation looks functional.

The Select component is correctly wired up with the form state. Minor note: both title and selectedItem perform the same find operation—you could extract this to a variable if preferred, but it's not critical.

web/app/utils/profileAvatars.tsx (2)

172-185: Hash function provides ~5,000 unique combinations.

With 44 adjectives × 114 nouns = 5,016 unique display names. This may lead to collisions for projects with many anonymous users. Consider whether this level of uniqueness is acceptable for your use case, or if you need to expand the word lists or add a numeric suffix for disambiguation.


207-216: ProfileAvatar component is well-structured.

Clean implementation using boring-avatars with deterministic avatars based on profileId. The wrapper div allows flexible styling via className.

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

31-31: SaltService integration looks correct.

The SaltService is properly imported and injected following the existing dependency injection pattern in the constructor.

Also applies to: 296-296

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

42-140: Well-structured ActivityCalendar component with appropriate memoization.

The component correctly uses useMemo for expensive computations (countMap, weeks, monthLabels) with proper dependency arrays. The week/day iteration logic handles edge cases appropriately.


195-229: Good implementation of display name and date formatting with proper null handling.

The component correctly handles the null details case with a Loader fallback, and memoizes the display name computation.


376-396: Sessions loading state handles edge cases well.

The conditional rendering properly handles the three states: loading with no sessions, empty sessions, and sessions with load-more capability.

backend/apps/community/src/analytics/analytics.controller.ts (3)

91-92: LGTM - Online visitors window constant is appropriately defined.

The 5-minute window aligns with typical real-time analytics requirements for determining active sessions.


982-996: Heartbeat session handling correctly validates session existence before extending TTL.

The flow properly checks for session existence and throws ForbiddenException if no session exists, preventing orphan heartbeats.


1022-1022: recordSession correctly placed after session ID generation.

The isHeartbeat: true flag appropriately marks this as a session-initiating pageview event.

backend/apps/cloud/src/analytics/salt.service.ts (2)

103-131: regenerateExpiredSalts implementation is correct for scheduled tasks.

The method properly iterates through rotation types, checks expiration, and persists new salts with Redis caching. Good logging for observability.


61-101: No race condition risk — rotation is the primary key.

The rotation field is marked as @PrimaryColumn, making it the primary key of the Salt entity. This enforces uniqueness at the database level, preventing duplicate salts from being created under concurrent load. The repository's upsert operation (save()) will handle concurrent requests safely.

Likely an incorrect or invalid review comment.

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

1212-1220: Salt rotation translation keys added correctly.

The options clearly explain the behavior of each rotation period setting.

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

669-695: Profiles API client wiring looks consistent and type-safe

The three new helpers (getProfiles, getProfile, getProfileSessions) mirror existing analytics/session APIs (encoding filters, forwarding x-password, and shaping responses to Profile / ProfileDetails / Session[]). URL encoding of profileId and optional period/from/to handling look correct. No issues from my side here.

Also applies to: 697-718, 720-746

backend/apps/community/src/analytics/salt.service.ts (1)

61-101: The rotation column is already a primary key—no action needed

The Salt entity defines rotation as @PrimaryColumn, ensuring only one row exists per rotation type. The save() and findOne() operations are correct and will function as expected. No verification or changes required.

Likely an incorrect or invalid review comment.

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

77-78: LGTM!

New DTO imports for profile-related endpoints are correctly added and align with the new profile management feature.


1163-1175: LGTM!

The profileId generation and session recording flow for error logging is correctly implemented, following the same pattern as other endpoints.


1303-1315: LGTM!

Profile ID generation and session recording for custom events correctly uses isPageview: false to distinguish from pageview events.


1379-1399: LGTM!

Heartbeat endpoint correctly updates session activity and extends TTL for existing sessions.


1427-1434: LGTM!

Pageview logging correctly generates profile ID and records the session with isPageview: true.


1569-1575: LGTM!

Noscript endpoint correctly handles profile ID generation without client-provided profileId, as JavaScript-disabled clients cannot send this parameter.


1849-1868: LGTM!

The getSession endpoint is properly structured with access control checks and timezone handling.


2003-2015: LGTM!

Good use of Promise.all to parallelize fetching profile details, top pages, activity calendar, and chart data for better performance.


2075-2076: Consider whether customEVFilterApplied should be used for profile sessions.

Similar to getProfiles, the fourth return value from getFiltersQuery is not captured. If custom event filters should affect profile session queries, this may need adjustment.


1919-1920: No action needed. The getFiltersQuery method returns 4 values, but customEVFilterApplied is not destructured in getProfilesList by design—the same pattern is consistently used across all endpoints (getSessions, getErrors, getPage, and getConversations), and customEVFilterApplied is not used anywhere in the codebase. This is not an inconsistency specific to profile listing.

Likely an incorrect or invalid review comment.

backend/apps/community/src/analytics/analytics.service.ts (5)

330-333: LGTM!

SaltService is properly injected into the constructor following NestJS dependency injection patterns.


1439-1441: LGTM!

The migration from session_durations to the new sessions table with dateDiff calculation is correctly implemented. The FINAL modifier ensures merged rows are used.


2270-2275: LGTM!

The sessions table join for duration calculation is correctly implemented with FINAL modifier and proper join conditions.


3655-3656: LGTM!

Session duration query correctly updated to use the new sessions table with FINAL modifier.


3857-3858: LGTM!

The session duration aggregation in getSessionsList correctly uses the new sessions table with proper averaging logic.

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

1115-1128: PSID and profile ID hashing approach looks solid

Using SHA‑256 → UInt64 for psid/profile IDs with per‑purpose salts and anon_/usr_ prefixes gives deterministic, non‑guessable identifiers while keeping ClickHouse types simple. The stripping of known prefixes from userSupplied before hashing also avoids accidental collisions between anonymous and identified profiles.

Also applies to: 1165-1200


3865-3900: Online user count query looks correct and reasonably defensive

The 30‑minute “online” window combining analytics and custom events with uniqExact(psid) and guarding on psid IS NOT NULL is straightforward, and the try/catch fallback to 0 on errors is acceptable for a best‑effort metric.

@Blaumaus Blaumaus merged commit fedda88 into swetrix-revamp Dec 5, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 10, 2025
6 tasks
This was referenced Dec 19, 2025
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