Skip to content

Comments

Goals analytics#446

Merged
Blaumaus merged 13 commits intoswetrix-revampfrom
goals
Dec 7, 2025
Merged

Goals analytics#446
Blaumaus merged 13 commits intoswetrix-revampfrom
goals

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Dec 6, 2025

Changes

Community Edition support

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

Database migrations

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

Documentation

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

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced Goals feature to track custom conversion events and pageviews with flexible matching options (exact, contains, regex).
    • Added Goals dashboard with analytics including conversion statistics, historical trends, conversion rates, and per-session breakdowns.
    • Goals management: create, edit, delete, and toggle goals active status.
    • Metadata filtering to refine goal matching based on custom attributes.

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

@Blaumaus Blaumaus self-assigned this Dec 6, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 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

This PR introduces a comprehensive Goals feature for tracking custom analytics events and pageviews. It adds a complete backend module with entity, CRUD operations, and ClickHouse-backed analytics (stats, charts, sessions), paired with a full-featured frontend UI including list views, expandable charts, and create/edit modals integrated into the project dashboard.

Changes

Cohort / File(s) Summary
Backend Goal Entity & Schema
backend/apps/cloud/src/goal/entity/goal.entity.ts, backend/migrations/mysql/2025_12_07_goals.sql
Introduces Goal entity with type (pageview/custom_event), matchType (exact/contains/regex), metadata filters, and Project relation; creates corresponding MySQL table with schema and foreign key constraint.
Backend Goal DTOs
backend/apps/cloud/src/goal/dto/goal.dto.ts
Defines DTOs for goal CRUD: CreateGoalDto, UpdateGoalDto, GoalDto, GoalStatsDto, and MetadataFilterDto with nested validation and API documentation.
Backend Goal Service & Module
backend/apps/cloud/src/goal/goal.service.ts, backend/apps/cloud/src/goal/goal.module.ts
Implements GoalService with paginate, CRUD, and query methods; GoalModule wires entity, dependencies, and controller.
Backend Goal Controller
backend/apps/cloud/src/goal/goal.controller.ts
Adds comprehensive REST API with CRUD endpoints, permission checks, and analytics queries (stats, charts, sessions) backed by ClickHouse; supports time zones, date ranges, and pagination.
Backend App Integration
backend/apps/cloud/src/app.module.ts, backend/apps/cloud/src/project/entity/project.entity.ts
Registers GoalModule in AppModule; adds OneToMany goals relation to Project entity.
Frontend Goal API Layer
web/app/api/index.ts
Exports Goal interfaces (Goal, GoalStats, GoalChartData, GoalSession), CreateGoal type, and API functions (getProjectGoals, getGoal, createGoal, updateGoal, deleteGoal, getGoalStats, getGoalChart, getGoalSessions).
Frontend Goal Models & Constants
web/app/lib/models/Goal.ts, web/app/lib/constants/index.ts
Defines Goal type definitions and label mappings; adds "goals" tab to PROJECT_TABS.
Frontend Goal UI Components
web/app/pages/Project/Goals/View/GoalSettingsModal.tsx, web/app/pages/Project/Goals/View/GoalsView.tsx, web/app/pages/Project/Goals/View/index.tsx
Introduces GoalSettingsModal for creating/editing goals with metadata filters; GoalsView for listing, searching, expanding, and charting goals; includes per-goal stats and session data.
Frontend Project Integration
web/app/pages/Project/View/ViewProject.tsx, web/app/pages/Project/View/components/ProjectSidebar.tsx
Adds Goals tab to project dashboard; updates sidebar navigation with goals grouping; adds goalsRefreshTrigger context; refactors loading and pagination logic.
Frontend UI & Styling
web/app/ui/ProgressRing.tsx, web/app/ui/Text.tsx, web/app/styles/tailwind.css, web/app/pages/Project/View/components/CaptchaView.tsx
Introduces ProgressRing circular progress indicator; extends Text component with code styling; adds header-height CSS variables; updates CaptchaView loading state with LoadingBar.
Frontend Settings & Styling
web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/modals/NewFunnel.tsx
Adds selectedItem control to captcha difficulty dropdown; updates NewFunnel remove-step button styling.
Localization
web/public/locales/en.json
Adds UI text for goals feature (create, edit, delete, metadata, types) and security label; goal-related field labels and descriptions.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Frontend UI
    participant API as Frontend API
    participant Controller as Goal Controller
    participant Service as Goal Service
    participant DB as TypeORM Repo
    participant CH as ClickHouse
    
    User->>UI: Create new goal
    UI->>API: createGoal(goalData)
    API->>Controller: POST /goal (validate, check permissions)
    Controller->>Service: create(goalData)
    Service->>DB: save Goal entity
    DB-->>Service: Goal persisted
    Service-->>Controller: Goal
    Controller-->>API: 201 Goal response
    API-->>UI: Goal created
    
    User->>UI: View goal stats & chart
    UI->>API: getGoalStats(goalId, period)
    API->>Controller: GET /goal/:id/stats (resolve timezone)
    Controller->>CH: Query conversions, sessions (filtered by goal type/metadata)
    CH-->>Controller: Aggregated metrics
    Controller->>Controller: Calculate conversion rate & trend
    Controller-->>API: GoalStats
    API-->>UI: Stats rendered
    
    UI->>API: getGoalChart(goalId, period, timeBucket)
    API->>Controller: GET /goal/:id/chart (group by time bucket)
    Controller->>CH: Query time-series data (by timeBucket: day/hour/month)
    CH-->>Controller: Time-bucketed conversions & sessions
    Controller->>Controller: Format labels & align dates
    Controller-->>API: GoalChartData { x, conversions, uniqueSessions }
    API-->>UI: Chart data plotted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Goal Controller analytics logic (backend/apps/cloud/src/goal/goal.controller.ts): Dense ClickHouse query construction with multiple conditional branches for goal type (custom_event vs. pageview), metadata filtering, time-bucket grouping, and timezone handling; verify correctness of aggregation logic and performance of queries.
  • GoalsView component state management (web/app/pages/Project/Goals/View/GoalsView.tsx): Complex multi-level state for goals, per-goal stats, per-goal chart data, pagination, and refresh logic; verify loading state consistency and data fetching edge cases (empty states, errors, pagination transitions).
  • ViewProject context and refresh coordination (web/app/pages/Project/View/ViewProject.tsx): Multiple refactored loading and pagination helpers now use skip-based resets; verify that all refresh flows (stats, sessions, errors, profiles, goals) correctly reset pagination and load fresh data without duplication or stale state.
  • Permission and authorization checks in controller: Ensure ProjectService permission checks (view/manage) are consistently applied across all endpoints and that error handling (NotFoundException, ForbiddenException) is comprehensive.

Possibly related PRs

  • UI updates #441: Modifies NewFunnel.tsx component styling (remove-step button className update) — touches same file as this PR.
  • Revive captcha #443: Modifies Project entity to change captcha-related fields — overlaps with this PR's addition of goals OneToMany relation to the same entity.

Poem

🐰 Goals now bloom in the analytics grove,
With custom events and pageviews we love,
Charts dance and stats sing, conversions take flight,
A feature so goal-ful, shining so bright! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides a reference to the related issue but does not adequately fill out the required template sections. The checklist items are present but all remain unchecked, leaving critical information about Community Edition support, database migrations, and documentation status unclear. Complete the PR description by checking the appropriate boxes and providing clear statements about which checklist items apply to this Goals analytics feature (e.g., confirm if this is Community Edition or Enterprise-only, clarify which database migrations were added).
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature being added: Goals analytics. It directly summarizes the primary change across the entire changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

❤️ Share

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

@Blaumaus Blaumaus marked this pull request as ready for review December 6, 2025 23:34
@Blaumaus
Copy link
Member Author

Blaumaus commented Dec 6, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 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: 9

🧹 Nitpick comments (21)
web/app/modals/NewFunnel.tsx (1)

131-131: Inconsistent Tailwind variant ordering.

The className mixes hover:dark: and dark:hover: orderings. For consistency with the other dark mode hover utilities in this same line (dark:hover:bg-slate-800, dark:hover:text-slate-300), consider using dark:hover:border-slate-700/80.

-                  className='rounded-md border border-transparent p-1.5 text-gray-800 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-slate-400 hover:dark:border-slate-700/80 dark:hover:bg-slate-800 dark:hover:text-slate-300'
+                  className='rounded-md border border-transparent p-1.5 text-gray-800 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:text-slate-400 dark:hover:border-slate-700/80 dark:hover:bg-slate-800 dark:hover:text-slate-300'
web/app/pages/Project/Settings/ProjectSettings.tsx (1)

1029-1029: Provide a complete label in selectedItem for consistency.

The selectedItem prop has an empty label property, which is inconsistent with other Select usages in this file (lines 757, 937-939) where the full item object is provided. While the title prop handles display, the empty label may cause unexpected behavior in the Select component's internal selection matching logic.

Apply this diff to find and use the complete item:

+                        const difficultyItems = [
+                          { value: 2, label: t('project.settings.captcha.difficultyLevels.veryEasy') },
+                          { value: 3, label: t('project.settings.captcha.difficultyLevels.easy') },
+                          { value: 4, label: t('project.settings.captcha.difficultyLevels.medium') },
+                          { value: 5, label: t('project.settings.captcha.difficultyLevels.hard') },
+                          { value: 6, label: t('project.settings.captcha.difficultyLevels.veryHard') },
+                        ]
                         <Select
                           label={t('project.settings.captcha.difficulty')}
                           hint={t('project.settings.captcha.difficultyHint')}
                           className='lg:w-1/2'
                           hintClassName='lg:w-2/3'
-                          items={[
-                            { value: 2, label: t('project.settings.captcha.difficultyLevels.veryEasy') },
-                            { value: 3, label: t('project.settings.captcha.difficultyLevels.easy') },
-                            { value: 4, label: t('project.settings.captcha.difficultyLevels.medium') },
-                            { value: 5, label: t('project.settings.captcha.difficultyLevels.hard') },
-                            { value: 6, label: t('project.settings.captcha.difficultyLevels.veryHard') },
-                          ]}
+                          items={difficultyItems}
                           keyExtractor={(item) => String(item.value)}
                           labelExtractor={(item) => item.label}
-                          selectedItem={{ value: captchaDifficulty, label: '' }}
+                          selectedItem={difficultyItems.find((item) => item.value === captchaDifficulty)}
                           onSelect={(item: { value: number; label: string }) => {
                             setCaptchaDifficulty(item.value)
                           }}
web/app/ui/ProgressRing.tsx (3)

60-60: Optionally guard against non‑finite value before clamping

If value ever becomes NaN or otherwise non‑finite (e.g., from a division by zero upstream), it will propagate through normalizedValue and into the SVG attributes. Consider a small defensive guard to fall back to 0 in that case:

-  const normalizedValue = useMemo(() => Math.min(100, Math.max(0, value)), [value])
+  const normalizedValue = useMemo(() => {
+    const safe = Number.isFinite(value) ? value : 0
+    return Math.min(100, Math.max(0, safe))
+  }, [value])

This keeps the ring in a sane state even when upstream data is temporarily bad.


62-72: Clamp radius to avoid negative values when size < strokeWidth

If a caller ever passes a size smaller than strokeWidth, radius becomes negative, which can produce odd rendering across browsers. A tiny clamp keeps things safe with no behavioral change for normal inputs:

-    const r = (size - strokeWidth) / 2
+    const r = Math.max(0, (size - strokeWidth) / 2)

circumference and strokeDashoffset then gracefully collapse to 0 when the ring can’t be drawn.


79-111: Consider optional ARIA hooks for better accessibility

Right now the ring is purely visual; screen readers won’t get any progress information unless it’s duplicated elsewhere. Consider (in a later pass) adding optional props for accessibility, e.g. an ariaLabel or a label + wiring role="progressbar" with aria-valuenow/min/max, so consumers can opt into semantic progress when needed.

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

16-21: Unify height variables to avoid duplicated header constants

You now have --header-height plus hard-coded 74px/157px in other min-height vars. To keep layouts consistent when header size changes, consider deriving --min-height-page and --min-height-min-footer from --header-height instead of fixed pixel values.

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

822-887: Dashboard security/goals labels look good; consider deduplicating description text

The new dashboard.security, dashboard.goals, and dashboard.goalsDesc keys read clearly and match the new tab semantics. Since dashboard.goalsDesc and goals.description describe essentially the same concept, you might want to reuse a single translation string to avoid drift if the copy is updated in future.


953-1000: Goals locale block is comprehensive; minor wording consistency nits only

The goals section covers all the UI states (CRUD, filters, stats, sessions) and aligns well with the feature surface. Two tiny optional polish points:

  • add / addGoal could be consolidated or named more explicitly based on usage (e.g. one for CTA button, one for modal title) to avoid confusion.
  • Ensure capitalization is consistent across labels (e.g. “Add goal” vs “Add Goal”) per your UI copy guidelines.

Functionally everything looks fine.

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

1-14: Goals table schema matches the model; consider tightening projectId constraints

The table shape (including type, matchType, metadataFilters JSON, and projectId FK with ON DELETE CASCADE) matches the described Goal model well. Two potential hardening tweaks if they fit the domain:

  • If every goal must belong to a project, consider making projectId NOT NULL to enforce that invariant at the DB level.
  • You’ll query goals by projectId a lot; InnoDB will have an index for the FK, but it can be worth explicitly documenting/ensuring that in your migrations (and possibly adding a composite index like (projectId, active) if you often filter by both).

Please verify these against your expected query patterns and CE vs cloud usage before changing the migration.

web/app/pages/Project/Goals/View/GoalSettingsModal.tsx (3)

56-72: Consider handling component unmount during async operation.

If the modal is closed while loadGoal is still in progress, state updates will be attempted on an unmounted component. Consider adding an abort mechanism or checking if the component is still mounted before updating state.

 const loadGoal = async () => {
   if (!goalId) return
   setIsLoading(true)
   try {
     const goal = await getGoal(goalId)
+    // If modal was closed during fetch, skip state updates
+    if (!isOpen) return
     setName(goal.name)
     setType(goal.type)
     setMatchType(goal.matchType)
     setValue(goal.value || '')
     setMetadataFilters(goal.metadataFilters || [])
   } catch (err: any) {
     toast.error(err?.message || 'Failed to load goal')
     onClose()
   } finally {
     setIsLoading(false)
   }
 }

Alternatively, use an AbortController or a mounted ref pattern.


120-132: Consider validating metadata filters before submission.

Empty metadata filter entries (with empty key or value) can be added and submitted. Consider filtering out incomplete entries or adding validation to prevent submitting filters with empty keys/values.

 const handleSubmit = async (e: React.FormEvent) => {
   e.preventDefault()
   setIsSaving(true)

+  // Filter out incomplete metadata filters
+  const validFilters = metadataFilters.filter(f => f.key.trim() && f.value.trim())

   try {
     if (isNew) {
       const data: CreateGoal = {
         pid: projectId,
         name,
         type,
         matchType,
         value: value || undefined,
-        metadataFilters: metadataFilters.length > 0 ? metadataFilters : undefined,
+        metadataFilters: validFilters.length > 0 ? validFilters : undefined,
       }
       await createGoal(data)

243-268: Consider adding accessible labels for metadata filter inputs.

The metadata filter inputs lack aria-label or associated <label> elements, which may affect screen reader accessibility.

 <Input
   value={filter.key}
   onChange={(e) => updateMetadataFilter(index, 'key', e.target.value)}
   placeholder={t('goals.filterKey')}
   className='flex-1'
+  aria-label={`${t('goals.filterKey')} ${index + 1}`}
 />
 <span className='text-gray-500'>=</span>
 <Input
   value={filter.value}
   onChange={(e) => updateMetadataFilter(index, 'value', e.target.value)}
   placeholder={t('goals.filterValue')}
   className='flex-1'
+  aria-label={`${t('goals.filterValue')} ${index + 1}`}
 />
backend/apps/cloud/src/goal/dto/goal.dto.ts (1)

106-150: Consider adding validation decorators to response DTOs for documentation completeness.

While GoalDto and GoalStatsDto are response types and don't require runtime validation, adding validation decorators would improve Swagger documentation and provide consistency. This is optional since these are output DTOs.

web/app/pages/Project/Goals/View/GoalsView.tsx (1)

524-530: Consider debouncing or batching stats loading for performance.

When the date range changes, loadGoalStats is called for every goal in the list. With many goals, this could trigger numerous parallel API requests. Consider batching these requests or implementing a debounce.

+import { useDebouncedCallback } from 'use-debounce'

// Or batch the requests:
 useEffect(() => {
   // Load stats for all goals when goals change or date range changes
-  goals.forEach((goal) => {
-    loadGoalStats(goal.id)
-  })
+  const loadAllStats = async () => {
+    await Promise.all(goals.map((goal) => loadGoalStats(goal.id)))
+  }
+  loadAllStats()
   // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [goals, period, from, to, timezone])

Using Promise.all at least allows the browser to batch connection handling, though a dedicated batch endpoint would be more efficient.

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

68-71: Add onDelete cascade behavior to the Project relation for referential integrity consistency.

The @ManyToOne relation to Project lacks cascade delete behavior. The codebase consistently applies onDelete: 'CASCADE' for similar child entities—for example, ProjectViewEntity specifies { onDelete: 'CASCADE' } when relating to Project. Without this, orphaned goals will remain in the database if a project is deleted.

 @ApiProperty({ type: () => Project })
-@ManyToOne(() => Project, project => project.goals)
+@ManyToOne(() => Project, project => project.goals, { onDelete: 'CASCADE' })
 @JoinColumn()
 project: Project
web/app/lib/models/Goal.ts (1)

1-33: Unify Goal types across models and API, especially GoalChartData naming

This file defines the canonical Goal types, while web/app/api/index.ts redefines Goal, GoalStats, GoalChartData, and GoalSession with slightly different shapes (notably, GoalChartData here is a single point, but the API version is an x/conversions/uniqueSessions series). That duplication will drift and the shared name for different shapes is easy to misuse.

Consider:

  • Importing Goal, GoalStats, and GoalSession from here in the API layer instead of redefining them.
  • Renaming the API’s { x: string[]; conversions: number[]; ... } to something like GoalChartSeries and using GoalChartData[] from this file as the per-point shape inside the UI.

This keeps a single source of truth for models and avoids subtle type mismatches between API responses and view models.

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

1-72: Type create/update with DTOs instead of Partial<Goal>

The service methods are fine functionally, but create and update currently accept Partial<Goal>, which is quite permissive and bypasses the structure you already enforce at the controller/DTO level.

To tighten things up, consider:

async create(goalData: CreateGoalDto): Promise<Goal> { ... }
async update(id: string, goalData: UpdateGoalDto): Promise<UpdateResult> { ... }

This keeps the service surface aligned with your DTO contracts and makes later changes (like adding non-persisted fields to Goal) safer.

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

545-545: Goals refresh uses a trigger counter but isn’t tied to global loading

In refreshStats, when activeTab === PROJECT_TABS.goals you only bump goalsRefreshTrigger. That’s a clean, side-effect-free signal for GoalsView to refetch.

One small UX consideration: since goals fetching doesn’t touch dataLoading, the global <LoadingBar /> you added won’t appear during goal reloads; any loading indication will have to live inside GoalsView. If you want consistent behavior with other tabs, you may want to either:

  • Expose a setter for dataLoading via context for GoalsView, or
  • Let ViewProjectContent own the goals fetch and keep GoalsView dumb.

Not blocking, just something to keep in mind.

Also applies to: 2620-2672, 3413-3416

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

1137-1282: Reuse shared Goal types and disambiguate GoalChartData

Functionally, the new Goals API helpers are aligned with the rest of this file (same error handling, pagination shape, and axios usage). A couple of type-structure points are worth tightening:

  1. Type duplication vs ~/lib/models/Goal

    Goal, GoalStats, and GoalSession are already defined in web/app/lib/models/Goal.ts with the same fields. Redefining them here risks future drift.

    Suggestion:

    import type { Goal, GoalStats, GoalSession } from '~/lib/models/Goal'

    and drop the local interface duplicates.

  2. GoalChartData name clash for different shapes

    Here, GoalChartData is a series:

    { x: string[]; conversions: number[]; uniqueSessions: number[] }

    whereas in ~/lib/models/Goal it describes a single point:

    { time: string; conversions: number; uniqueSessions: number }

    Sharing the same name for two incompatible shapes is error-prone.

    Consider renaming this one to something like GoalChartSeries and typing the helper accordingly:

    export interface GoalChartSeries {
      x: string[]
      conversions: number[]
      uniqueSessions: number[]
    }
    
    export const getGoalChart = (...) =>
      api
        .get(`/goal/${goalId}/chart`, { params: { ... } })
        .then((response): { chart: GoalChartSeries } => response.data)

    Then the UI can map that into an array of the shared GoalChartData point type if needed.

If you centralize types in ~/lib/models/Goal and use distinct names for series vs points, the goals feature will be much easier to maintain.

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

124-128: Address TypeScript type mismatch instead of suppressing.

The @ts-expect-error indicates a type mismatch when mapping results. Consider defining a proper return type or using a DTO transformation to avoid suppressing type checking.

-    // @ts-expect-error
-    result.results = _map(result.results, goal => ({
+    const transformedResults = _map(result.results, goal => ({
       ..._omit(goal, ['project']),
       pid: goal.project.id,
-    }))
+    })) as GoalDto[]
 
-    return result
+    return {
+      ...result,
+      results: transformedResults,
+    }

284-287: Remove unused _table parameter.

The _table parameter is never used in the method body. Remove it to avoid confusion.

   private buildGoalMatchCondition(
     goal: Goal,
-    _table: 'analytics' | 'customEV',
   ): { condition: string; params: Record<string, string> } {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee47c35 and effca2c.

📒 Files selected for processing (23)
  • backend/apps/cloud/src/app.module.ts (2 hunks)
  • backend/apps/cloud/src/goal/dto/goal.dto.ts (1 hunks)
  • backend/apps/cloud/src/goal/entity/goal.entity.ts (1 hunks)
  • backend/apps/cloud/src/goal/goal.controller.ts (1 hunks)
  • backend/apps/cloud/src/goal/goal.module.ts (1 hunks)
  • backend/apps/cloud/src/goal/goal.service.ts (1 hunks)
  • backend/apps/cloud/src/project/entity/project.entity.ts (2 hunks)
  • backend/migrations/mysql/2025_12_07_goals.sql (1 hunks)
  • web/app/api/index.ts (1 hunks)
  • web/app/lib/constants/index.ts (2 hunks)
  • web/app/lib/models/Goal.ts (1 hunks)
  • web/app/modals/NewFunnel.tsx (1 hunks)
  • web/app/pages/Project/Goals/View/GoalSettingsModal.tsx (1 hunks)
  • web/app/pages/Project/Goals/View/GoalsView.tsx (1 hunks)
  • web/app/pages/Project/Goals/View/index.tsx (1 hunks)
  • web/app/pages/Project/Settings/ProjectSettings.tsx (1 hunks)
  • web/app/pages/Project/View/ViewProject.tsx (34 hunks)
  • web/app/pages/Project/View/components/CaptchaView.tsx (2 hunks)
  • web/app/pages/Project/View/components/ProjectSidebar.tsx (6 hunks)
  • web/app/styles/tailwind.css (1 hunks)
  • web/app/ui/ProgressRing.tsx (1 hunks)
  • web/app/ui/Text.tsx (2 hunks)
  • web/public/locales/en.json (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
backend/apps/cloud/src/project/entity/project.entity.ts (2)
web/app/api/index.ts (1)
  • Goal (1138-1148)
web/app/lib/models/Goal.ts (1)
  • Goal (9-19)
web/app/ui/ProgressRing.tsx (1)
web/app/utils/generic.ts (1)
  • cn (150-152)
web/app/ui/Text.tsx (1)
web/app/utils/generic.ts (1)
  • cn (150-152)
web/app/pages/Project/View/ViewProject.tsx (2)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (422-424)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
  • getFormatDate (1944-1944)
web/app/pages/Project/Goals/View/GoalSettingsModal.tsx (1)
web/app/api/index.ts (4)
  • getGoal (1191-1197)
  • CreateGoal (1199-1206)
  • createGoal (1208-1214)
  • updateGoal (1216-1222)
backend/apps/cloud/src/goal/dto/goal.dto.ts (2)
backend/apps/cloud/src/goal/entity/goal.entity.ts (1)
  • MetadataFilter (23-26)
web/app/lib/models/Goal.ts (3)
  • MetadataFilter (4-7)
  • GoalType (1-1)
  • GoalMatchType (2-2)
backend/apps/cloud/src/goal/goal.service.ts (2)
web/app/api/index.ts (1)
  • Goal (1138-1148)
web/app/lib/models/Goal.ts (1)
  • Goal (9-19)
web/app/pages/Project/View/components/ProjectSidebar.tsx (2)
web/app/ui/Text.tsx (1)
  • Text (51-75)
web/app/lib/constants/index.ts (1)
  • PROJECT_TABS (422-424)
web/app/lib/models/Goal.ts (2)
backend/apps/cloud/src/goal/entity/goal.entity.ts (1)
  • MetadataFilter (23-26)
web/app/api/index.ts (4)
  • Goal (1138-1148)
  • GoalStats (1150-1156)
  • GoalChartData (1158-1162)
  • GoalSession (1164-1172)
🔇 Additional comments (38)
web/app/ui/Text.tsx (2)

17-17: New code prop is a clean, backwards-compatible extension

Adding code?: boolean to TextProps is type-safe and optional, so it won’t break existing call sites while giving a clear, semantic flag for “code-like” text.


58-70: Conditional code styling is correctly scoped and merged

Destructuring code and omitting it from {...props} avoids leaking an invalid DOM attribute, and the conditional Tailwind classes are properly merged via cn alongside existing size/weight/colour/truncate handling. This is a straightforward, non-breaking enhancement.

web/app/ui/ProgressRing.tsx (1)

5-50: Threshold logic and color mapping look solid

The ColourThresholds shape and getColourClasses logic cleanly partition values into green/amber/red without gaps or overlaps, and the defaults are reasonable. No changes needed here.

web/app/pages/Project/View/components/CaptchaView.tsx (2)

15-15: LoadingBar import wiring looks correct

Importing LoadingBar from ~/ui/LoadingBar is consistent with the usage below; no issues here.


172-187: Loading state split between initial load and refresh looks solid

The hasExistingData flag plus the two guards:

  • analyticsLoading && !hasExistingData → full-page <Loader /> only on initial load
  • isPanelsDataEmpty && !dataLoadingNoEvents only when not actively fetching
  • dataLoading && hasExistingData → inline <LoadingBar /> while refreshing with prior data

give a clear, non-flickering UX and don’t introduce new state bugs given the current panelsData/chartData handling.

web/app/lib/constants/index.ts (1)

400-425: Confirm Goals availability for self‑hosted before exposing tab

Adding goals to both SELFHOSTED_PROJECT_TABS and PRODUCTION_PROJECT_TABS keeps the PROJECT_TABS typing consistent and will surface the Goals tab everywhere. That’s good for parity, but it assumes the Goals backend and migrations are also enabled for Community Edition/self‑hosted deployments. Please double‑check that self‑hosted instances actually have the necessary API/migrations wired; otherwise consider gating the goals tab by edition or feature flag.

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

1-24: GoalModule wiring looks idiomatic; just keep an eye on circular deps

Importing TypeOrmModule.forFeature([Goal]) along with ProjectModule, AppLoggerModule, UserModule, and AnalyticsModule, and exporting GoalService is a standard NestJS pattern and should work fine. Please just verify that ProjectModule (or others) do not in turn import GoalModule, to avoid circular dependency warnings at runtime.

web/app/pages/Project/Goals/View/index.tsx (1)

1-1: Barrel re‑export for GoalsView is clean and convenient

Re‑exporting the default from ./GoalsView keeps the import path concise (.../Goals/View) and matches common project conventions. No issues here.

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

12-12: Project–Goal relation looks correct; verify GDPR export & entity/table alignment

The @OneToMany(() => Goal, goal => goal.project) plus goals: Goal[] aligns with the new goal.projectId FK and gives you the inverse navigation on Project. A couple of follow‑ups:

  • Line 28 notes that modifying properties here may require updates to the GDPR data export email template. If goals should appear in a user’s export, ensure that template is updated accordingly (or explicitly decide they’re out of scope).
  • Please also confirm that Goal is mapped to the same table name and column (projectId) as defined in the 2025_12_07_goals.sql migration so TypeORM doesn’t drift from the schema over time.

Also applies to: 127-130

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

23-23: GoalModule registration in AppModule looks fine; confirm if it should be configurable

Importing GoalModule and adding it to the shared modules array correctly wires the new feature into the cloud app. If Goals are meant to be always‑on for all cloud environments, this is perfect; if not, consider guarding it behind an env flag similar to ENABLE_INTEGRATIONS.

Also applies to: 81-81

web/app/pages/Project/Goals/View/GoalSettingsModal.tsx (1)

134-145: LGTM!

Good use of Headless UI Dialog with proper transition effects and responsive styling. The dark mode support is well-implemented.

backend/apps/cloud/src/goal/dto/goal.dto.ts (3)

15-25: LGTM!

The MetadataFilterDto correctly implements the MetadataFilter interface with proper validation decorators (@IsString, @IsNotEmpty) ensuring both key and value are non-empty strings.


27-62: LGTM!

The CreateGoalDto is well-structured with appropriate validation decorators. The nested validation for metadataFilters using @ValidateNested({ each: true }) and @Type(() => MetadataFilterDto) ensures proper validation of array items.


64-104: LGTM!

The UpdateGoalDto correctly makes all fields optional for partial updates while maintaining the same validation rules. The inclusion of the active boolean field allows toggling goal status.

web/app/pages/Project/View/components/ProjectSidebar.tsx (4)

25-25: LGTM!

Good addition of the goals color token to ICON_COLORS map, maintaining consistency with the existing color scheme for analytics tabs.


63-63: Verify the UX impact of always-expanded groups.

The CollapsibleGroup now defaults to isExpanded = true instead of using group.defaultExpanded. This means all groups will be expanded by default, which may affect the user experience if there are many groups/tabs.

Was this intentional as part of the sidebar simplification? If some groups should be collapsed by default (e.g., Security), consider restoring the defaultExpanded logic or making it configurable.


174-189: LGTM!

The Goals tab is correctly added to the productAnalyticsTabs filter, integrating it into the Product Analytics group alongside profiles, sessions, funnels, and errors.


191-198: LGTM!

Renaming the CAPTCHA group to "Security" is a reasonable UX improvement that provides a more general category name that could accommodate future security-related tabs.

web/app/pages/Project/Goals/View/GoalsView.tsx (3)

253-260: LGTM!

Good use of useMemo for deriving the pattern display with match type prefixes. The conditional rendering handles null values appropriately.


700-709: LGTM!

Good conditional rendering for pagination - only shown when there are multiple pages and no active filter. This prevents confusing pagination behavior when filtering locally.


534-540: The eslint-disable comment here is justified and necessary. The effect intentionally excludes goalChartData from dependencies to prevent infinite loops when loadGoalChartData updates the state. The condition expandedGoalId && goalChartData[expandedGoalId] is designed to prevent unnecessary reloads, and the handler handleToggleExpand (line 513-514) manages initial data loading when a goal is expanded. The dependencies [period, from, to, timezone, timeBucket] correctly capture when chart data should be refreshed for the currently expanded goal. No infinite loop risk exists with this pattern.

Likely an incorrect or invalid review comment.

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

12-21: LGTM!

The enums GoalType and GoalMatchType are well-defined with clear string values that align with the frontend type definitions in web/app/lib/models/Goal.ts.


28-76: LGTM!

The Goal entity is well-structured with:

  • Proper UUID primary key generation
  • Appropriate column lengths matching DTO validation
  • Sensible defaults (matchType: EXACT, active: true)
  • Nullable fields where appropriate (value, metadataFilters)
  • ApiProperty decorators for Swagger documentation
web/app/pages/Project/View/ViewProject.tsx (7)

41-43: Goals tab and view wiring looks consistent

The new TargetIcon, GoalsView import, goals tab entry (PROJECT_TABS.goals), and conditional <GoalsView /> render are all wired in the same pattern as existing tabs (funnels, captcha, alerts). Passing period, and from/to formatted via getFormatDate, plus timezone, aligns with how other analytic endpoints are called.

Assuming GoalsView consumes period/date range similarly to the goals endpoints in ~/api, this integration looks good.

Also applies to: 1503-1507, 1532-1535, 4304-4311, 170-170


392-416: Goals refresh trigger is correctly plumbed through context

Adding goalsRefreshTrigger to ViewProjectContextType, the default context, state (useState(0)), and provider value is consistent with the existing captchaRefreshTrigger pattern. This gives child components (like GoalsView) a simple numeric “bump” they can watch for reloads without threading callbacks.

No issues here.

Also applies to: 418-437, 539-546, 3413-3416


2026-2043: Session pagination reset + override logic is sound

The new loadSessions(forcedSkip?, override?) and its usage look correct:

  • On back from a session detail and in refreshStats for the sessions tab, you:
    • Reset sessionsSkip to 0.
    • Call loadSessions(0, true), which replaces the list instead of appending.
  • The guard using sessionsRequestIdRef prevents late responses from stale calls from corrupting state.
  • The “auto-load first page” effect respects skipNextSessionsAutoLoadRef so you don’t double-fetch when you’ve already explicitly reloaded.

This should fix the previous “resume pagination from old offset” behavior without introducing obvious races.

Also applies to: 2062-2114, 2788-2791, 3965-3990


2116-2189: Profiles pagination reset mirrors sessions and looks correct

Similarly for profiles:

  • loadProfiles(forcedSkip?, override?) now supports replacing vs appending.
  • When:
    • The profiles tab becomes active,
    • The profile type filter changes, or
    • You return from a profile detail,
      you reset profilesSkip to 0 and call loadProfiles(0, true).
  • skipNextProfilesAutoLoadRef prevents the “auto-load” effect from firing a redundant request after a manual reset.

This is a clear improvement over the prior pagination behavior.

Also applies to: 2794-2820, 2832-2844, 3895-3902


3023-3025: Resetting skip counters on period/filter/navigation changes is consistent

The various new setSessionsSkip(0), setErrorsSkip(0), and setProfilesSkip(0) calls (on period change, filter changes, and back-navigation from details) are consistent with the updated loaders that accept (forcedSkip, override).

Together they ensure:

  • New time ranges and filters start at the first page.
  • Returning from a detail view doesn’t resume from an old offset.

The patterns are uniform across sessions, errors, and profiles and look good.

Also applies to: 785-796, 2030-2034, 2048-2052, 2805-2807, 2839-2842


3426-3431: Extended LoadingBar conditions improve perceived responsiveness

Showing <LoadingBar /> when:

  • dataLoading && !isAutoRefreshing, or
  • A next page of sessions/errors/profiles is loading while the list is non-empty,

is a nice UX improvement: users see a subtle top bar when more data is coming, without blocking the initial skeleton-based loads. This logic matches how the list loaders work elsewhere in the component.


3270-3275: Layout switches to header-aware height utilities are coherent

Replacing the older min-h-min-footer / h-full patterns with min-h-including-header and h-including-header across the various early-return branches and the main layout keeps sidebar and content height consistent relative to the header.

All usages (locked state, empty data states, errors-only state, and main view) have been updated in sync, so there’s no obvious mismatch.

Also applies to: 3288-3296, 3330-3338, 3366-3374, 3441-3458, 3295-3309, 3334-3348, 3385-3388

backend/apps/cloud/src/goal/goal.controller.ts (8)

66-93: LGTM!

The endpoint correctly fetches the goal with relations, verifies user access via allowedToView, and returns a clean response with pid instead of nested project data.


215-260: LGTM!

The update flow correctly validates ownership, whitelists update fields with _pick, and returns fresh data after the update.


262-282: LGTM!

The delete endpoint follows the standard pattern with proper authorization checks before deletion.


325-351: LGTM!

The metadata condition builder correctly uses parameterized queries and ClickHouse array functions to safely filter by metadata key-value pairs.


353-485: LGTM!

The stats endpoint correctly computes analytics metrics with proper handling of edge cases (division by zero via || 1 fallback) and secure parameterized ClickHouse queries.


487-559: LGTM!

The helper methods correctly handle time bucket granularities and date string generation for chart data alignment.


681-781: LGTM!

The sessions endpoint efficiently uses Promise.all for parallel query execution and correctly aggregates session data with proper pagination.


47-53: No issue found. The timeBucketConversion map is complete and correctly covers all possible values returned by getLowestPossibleTimeBucket(). The function returns only MINUTE, HOUR, DAY, MONTH, or YEAR—never 'week'. The map entries (minute, hour, day, month, year) match all possible return values, so the fallback to 'toStartOfDay' will never be triggered by a missing 'week' entry.

Likely an incorrect or invalid review comment.

Comment on lines +154 to +159
format:
// @ts-expect-error
timeFormat === TimeFormat['24-hour']
? (x: string) => d3.timeFormat(tbsFormatMapper24h[timeBucket])(x as unknown as Date)
: (x: string) => d3.timeFormat(tbsFormatMapper[timeBucket])(x as unknown as Date),
},
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

🧩 Analysis chain

🏁 Script executed:

# Find TimeFormat type definition
rg -n "TimeFormat" --type=ts -B 2 -A 10 | head -50

Repository: Swetrix/swetrix

Length of output: 3056


🏁 Script executed:

# Check the specific file mentioned
head -20 web/app/lib/constants/index.ts

Repository: Swetrix/swetrix

Length of output: 624


🏁 Script executed:

# Also check if TimeFormat is defined in the GoalsView file itself or imported
rg -n "TimeFormat" web/app/pages/Project/Goals/View/GoalsView.tsx -B 2 -A 2

Repository: Swetrix/swetrix

Length of output: 977


Remove the unnecessary @ts-expect-error comment.

The same comparison timeFormat === TimeFormat['24-hour'] appears elsewhere in the file (lines 160, 179) without this suppression, indicating the code is valid TypeScript. The bracket notation on an enum is properly supported and doesn't produce type errors. Remove the comment on line 155 to maintain consistency.

🤖 Prompt for AI Agents
In web/app/pages/Project/Goals/View/GoalsView.tsx around lines 154 to 159,
remove the unnecessary "// @ts-expect-error" comment on line 155; the timeFormat
=== TimeFormat['24-hour'] comparison is valid TypeScript (as used elsewhere in
the file) so delete the suppression and keep the existing ternary format
function expressions intact, then run TypeScript/ESLint checks to confirm no
errors.

@Blaumaus Blaumaus merged commit 3e57a81 into swetrix-revamp Dec 7, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 8, 2025
6 tasks
@coderabbitai coderabbitai bot mentioned this pull request Dec 20, 2025
9 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jan 26, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant