Skip to content

Implement outbound link as a goal type#823

Open
stijnie2210 wants to merge 2 commits intorybbit-io:masterfrom
stijnie2210:feature/outbound-link-goal
Open

Implement outbound link as a goal type#823
stijnie2210 wants to merge 2 commits intorybbit-io:masterfrom
stijnie2210:feature/outbound-link-goal

Conversation

@stijnie2210
Copy link
Copy Markdown
Contributor

@stijnie2210 stijnie2210 commented Jan 22, 2026

Summary by CodeRabbit

  • New Features
    • Added an Outbound Goal type to track clicks to external URLs and measure conversions.
    • Support for outbound URL pattern matching with wildcards (e.g., "stripe.com", "https://docs.example.com/**").
    • UI updates: outbound goal option in goal creation form, outbound icon in goal cards, and a dedicated outbound pattern field.
    • URL suggestions/autocomplete when setting outbound patterns and validation to require a pattern for outbound goals.

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

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 22, 2026

@stijnie2210 is attempting to deploy a commit to the goldflag's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds a new "outbound" goal type across client, server, and DB: API types and UI updated to capture outboundUrlPattern; server-side validation and query logic extended to handle outbound pattern matching; DB schema/config updated to store outboundUrlPattern.

Changes

Cohort / File(s) Summary
Client API Types & Hooks
client/src/api/analytics/endpoints/goals.ts, client/src/api/analytics/hooks/goals/useCreateGoal.ts, client/src/api/analytics/hooks/goals/useUpdateGoal.ts
Extended goalType union to include "outbound"; added outboundUrlPattern?: string to goal/config request types.
Client UI Icons & Components
client/src/components/EventIcons.tsx, client/src/app/[site]/goals/components/GoalCard.tsx
Added OutboundIcon and rendered it for outbound goals; GoalCard now displays config.outboundUrlPattern for outbound type.
Client Goal Form
client/src/app/[site]/goals/components/GoalFormModal.tsx
Added outbound goal option, outbound URL pattern input with suggestions (uses useGetOutboundLinks), form init/reset logic, and validation to require pattern when goalType === "outbound".
Server: Create / Update Handlers
server/src/api/analytics/goals/createGoal.ts, server/src/api/analytics/goals/updateGoal.ts
Extended goalType enum to include outbound; added outboundUrlPattern schema and conditional validation; update handler refactored to parse siteId/goalId params and validate IDs.
Server: Querying / Aggregation
server/src/api/analytics/goals/getGoals.ts, server/src/api/analytics/goals/getGoalSessions.ts
Added outbound branch: validate outboundUrlPattern, convert to regex, and include event URL match logic in conversions/session queries; skip goal if pattern missing.
Database Schema
server/src/db/postgres/schema.ts
Added optional outboundUrlPattern?: string to goals.config JSONB type.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server
  participant DB
  Client->>Server: POST /goals { goalType: "outbound", config:{ outboundUrlPattern } }
  Server->>Server: validate body (outboundUrlPattern required for outbound)
  Server->>DB: INSERT goal record with config.outboundUrlPattern
  DB-->>Server: inserted goal
  Server-->>Client: 201 Created

  Note right of Server: Later, querying conversions
  Client->>Server: GET /goals?siteId=...
  Server->>DB: Aggregate events: COUNT IF(type='outbound' AND match(url, regex_from(outboundUrlPattern)))
  DB-->>Server: aggregation results
  Server-->>Client: return goals + conversion counts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code both near and far,
Added outbound goals to catch each star,
Patterns, wildcards, regex delight,
Icons green and suggestions bright,
Clicks to conversions — tracked just right! 🥕🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing outbound link as a new goal type. It directly reflects the primary purpose of the PR across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
client/src/api/analytics/endpoints/goals.ts (1)

53-66: Missing outboundUrlPattern in CreateGoalParams.config.

The Goal interface includes outboundUrlPattern in its config (line 21), but CreateGoalParams.config doesn't include it. This will cause TypeScript errors when attempting to create outbound goals, as the field won't be allowed in the request payload.

🐛 Proposed fix
 export interface CreateGoalParams {
   name?: string;
   goalType: "path" | "event" | "outbound";
   config: {
     pathPattern?: string;
     eventName?: string;
     eventPropertyKey?: string;
     eventPropertyValue?: string | number | boolean;
     propertyFilters?: Array<{
       key: string;
       value: string | number | boolean;
     }>;
+    outboundUrlPattern?: string;
   };
 }
🤖 Fix all issues with AI agents
In `@client/src/app/`[site]/goals/components/GoalFormModal.tsx:
- Around line 234-254: In GoalFormModal fix the mismatched braces and duplicated
clearing logic in the block that normalizes values before submit: remove the
stray closing braces and the duplicated "Clear legacy fields" section, then
consolidate the per-goalType clearing logic so values.config only clears fields
not relevant to the current values.goalType (references: GoalFormModal,
values.goalType, values.config, propertyFilters, useProperties). Specifically
ensure the "event" branch sets pathPattern/outboundUrlPattern to undefined and
conditionally sets config.propertyFilters from propertyFilters when
useProperties is true, the "outbound" branch clears event-related fields
(eventName/eventPropertyKey/eventPropertyValue), and finally perform a single,
well-placed cleanup that clears any legacy eventPropertyKey/eventPropertyValue
only when they are not applicable.
🧹 Nitpick comments (1)
client/src/app/[site]/goals/components/GoalCard.tsx (1)

89-107: Consider explicit type check instead of implicit else.

The else branch implicitly assumes any goal that isn't "path" or "event" must be "outbound". This could lead to incorrect rendering if additional goal types are introduced in the future.

♻️ Suggested improvement
-              ) : goal.goalType === "event" ? (
+              ) : goal.goalType === "event" ? (
                 <Tooltip>
                   <TooltipTrigger asChild>
                     <EventIcon />
                   </TooltipTrigger>
                   <TooltipContent>
                     <p>Event Goal</p>
                   </TooltipContent>
                 </Tooltip>
-              ) : (
+              ) : goal.goalType === "outbound" ? (
                 <Tooltip>
                   <TooltipTrigger asChild>
                     <OutboundIcon />
                   </TooltipTrigger>
                   <TooltipContent>
                     <p>Outbound Goal</p>
                   </TooltipContent>
                 </Tooltip>
+              ) : null}
               )}

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/src/app/`[site]/goals/components/GoalFormModal.tsx:
- Around line 183-186: The second useEffect that sets useProperties (triggered
when isOpen/goal changes) only checks legacy fields
(goal?.config.eventPropertyKey && goal?.config.eventPropertyValue) and
overwrites the correct state set earlier; update this effect (or consolidate
both useEffects into one) so it mirrors the logic used earlier by checking both
the new propertyFilters array (goal?.config.propertyFilters?.length) and the
legacy fields before calling setUseProperties, ensuring useProperties is true if
either the propertyFilters array has entries or both legacy fields exist (and
thus avoiding the race/overwrite).

Comment on lines +183 to +186
useEffect(() => {
if (isOpen) {
setUseProperties(!!goal?.config.eventPropertyKey && !!goal?.config.eventPropertyValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent useProperties initialization overwrites correct state.

Line 185 only checks legacy fields (eventPropertyKey && eventPropertyValue), but the earlier useEffect at lines 130-133 correctly checks both propertyFilters?.length and legacy fields. Since both effects run on isOpen/goal changes, this effect runs second and overwrites the correct value, causing the property filters UI to be hidden when editing goals that use the new propertyFilters array.

🐛 Proposed fix
   useEffect(() => {
     if (isOpen) {
-      setUseProperties(!!goal?.config.eventPropertyKey && !!goal?.config.eventPropertyValue);
+      const hasFilters = !!(
+        goal?.config.propertyFilters?.length ||
+        (goal?.config.eventPropertyKey && goal?.config.eventPropertyValue !== undefined)
+      );
+      setUseProperties(hasFilters);

       if ((isEditMode || isCloneMode) && goal) {

Alternatively, consider consolidating these two useEffects into one to avoid the race condition and reduce duplication.

📝 Committable suggestion

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

Suggested change
useEffect(() => {
if (isOpen) {
setUseProperties(!!goal?.config.eventPropertyKey && !!goal?.config.eventPropertyValue);
useEffect(() => {
if (isOpen) {
const hasFilters = !!(
goal?.config.propertyFilters?.length ||
(goal?.config.eventPropertyKey && goal?.config.eventPropertyValue !== undefined)
);
setUseProperties(hasFilters);
if ((isEditMode || isCloneMode) && goal) {
🤖 Prompt for AI Agents
In `@client/src/app/`[site]/goals/components/GoalFormModal.tsx around lines 183 -
186, The second useEffect that sets useProperties (triggered when isOpen/goal
changes) only checks legacy fields (goal?.config.eventPropertyKey &&
goal?.config.eventPropertyValue) and overwrites the correct state set earlier;
update this effect (or consolidate both useEffects into one) so it mirrors the
logic used earlier by checking both the new propertyFilters array
(goal?.config.propertyFilters?.length) and the legacy fields before calling
setUseProperties, ensuring useProperties is true if either the propertyFilters
array has entries or both legacy fields exist (and thus avoiding the
race/overwrite).

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