Skip to content

feat: tracking components, SDK option sync, pause toggle#9

Open
izadoesdev wants to merge 2 commits into
mainfrom
izadoesdev/custom-events-components
Open

feat: tracking components, SDK option sync, pause toggle#9
izadoesdev wants to merge 2 commits into
mainfrom
izadoesdev/custom-events-components

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Apr 19, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds three Framer code components (TrackClick, TrackSubmit, TrackView) generated into Databuddy.tsx. Each has property controls so designers configure the event name and properties from Framer's right sidebar.
  • New Custom Events section in the plugin previews the available trackers and installs/removes the file in one click. The card shows the same component list in both states so the install affordance isn't a jarring layout change.
  • Syncs the injected <script> with current SDK options: data-debug, data-disabled, data-sampling-rate, data-skip-patterns, data-mask-patterns, data-use-pixel, data-ignore-bot-detection, data-api-url. Drops removed scrollDepth option.
  • Adds a Pause / Resume toggle in the footer next to Install so tracking can be flipped without uninstalling.
  • Brand tint updated from #0099ff to the new Databuddy purple.
  • setPluginData now respects framer.isAllowedTo("setPluginData") so view-only collaborators don't hit silent failures.

Test plan

  • Open the plugin in a Framer project — Custom Events card renders with three rows and an "Add to project" button.
  • Click Add — Databuddy.tsx appears under Code Files, success toast says "3 components ready", card swaps to show "Find them in the Assets panel" + Remove.
  • Drag TrackClick onto a button on the canvas — right sidebar shows Event / Properties inputs; default event name is button_clicked.
  • Drag TrackSubmit onto a form, fire a submit, confirm the network tab shows the configured event.
  • Drag TrackView over a section, scroll into view, confirm the event fires once at the configured threshold.
  • Click Remove — file disappears, card returns to install state.
  • Toggle Pause in the footer — data-disabled="true" appears in the injected script; toggle Resume to clear it.
  • Set sampling rate, skip patterns, mask patterns, debug — confirm matching data-* attributes appear and the debug script URL switches.
  • Self-Hosted: set custom API URL — data-api-url appears on the script.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Debug Mode toggle for easier troubleshooting
    • Added Pause/Resume button to temporarily suspend tracking
    • Introduced component installation for custom event tracking
    • New advanced tracking options: Pixel Transport and Bot Detection settings
    • Custom API URL and filtering configuration support
  • Style

    • Updated theme accent colors and redesigned form layout

Adds three Framer code components (TrackClick, TrackSubmit, TrackView)
generated into Databuddy.tsx with property controls for sidebar config.
Syncs script attributes with current SDK options (debug, disabled,
samplingRate, skip/maskPatterns, usePixel, ignoreBotDetection,
customApiUrl). Adds Pause/Resume toggle in the footer and a Custom
Events section that previews the available trackers and installs them
into the project on demand.
@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR refactors styling classes, adds support for filtering and debug configuration options, introduces generalized collapsible components replacing self-hosted patterns, and implements permission-aware component file management through Framer API integrations for tracking component installation and dashboard menu customization.

Changes

Cohort / File(s) Summary
Styling & Theme
src/App.css, src/framer.css
Refactored CSS class names for accessibility (option-labeloption-row-bordered, self-hosted-*collapsible-*), removed obsolete status/divider styles, added new component card/list/footer styles, and updated theme accent color from blue (#0099ff) to purple (#453c7c/#6b5ca5).
UI Components
src/components/ui/label.tsx
Introduced new LabelWithTooltip component that combines label and tooltip rendering with info icon, consolidating tooltip display logic previously scattered in App.tsx.
Main Application Logic
src/App.tsx
Refactored option label rendering to use new LabelWithTooltip component, removed divider prop, expanded state with debug/filtering/pause configuration, added permission-aware installation/removal flows, integrated framer.setMenu for dashboard integration, and replaced "Misc" and self-hosted sections with "Filtering" inputs and "Custom Events" card.
Script & Settings Utilities
src/lib/script.ts, src/lib/settings.ts, src/util/integration.ts
Updated script attribute generation and validation (isValidHttpUrl added), extended settings options with customApiUrl, debug, disabled, samplingRate, skipPatterns, maskPatterns, added permission checks in settings persistence, replaced scrollDepth tracking option with usePixel and ignoreBotDetection, and updated webVitals metrics list.
Component Management
src/lib/components.ts, src/hooks/use-component-file.ts
Added new module for managing Framer code components (TrackClick, TrackSubmit, TrackView with event tracking via window.databuddy?.track()), install/removal/subscription helpers, and introduced useComponentFile hook for component file state synchronization.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/App
    participant App as App.tsx
    participant Perms as useIsAllowedTo
    participant Components as components.ts
    participant Framer as Framer API
    participant Settings as settings.ts

    User->>App: Click Install Components
    App->>Perms: Check "setPluginData" permission
    Perms-->>App: Permission granted/denied
    alt Permission Denied
        App->>User: Show disabled button with tooltip
    else Permission Granted
        App->>Components: installComponents()
        Components->>Components: Locate/create Databuddy.tsx
        Components->>Framer: Create or update CodeFile
        Framer-->>Components: CodeFile result
        Components->>Settings: Save installation state
        Settings->>Framer: setPluginData with permission check
        Framer-->>Settings: Persisted
        Components-->>App: InstallResult {status, componentCount}
        App->>User: Navigate to component + show success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 From blue to purple, the colors align,
New components to track with a fine design,
Permission checks guard what can be done,
Collapsibles unified—refactoring fun!
Filters and dashboards dance as one. ✨

🚥 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 clearly and concisely summarizes the three main features added: tracking components, SDK option synchronization, and a pause toggle—all core changes reflected throughout the PR.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch izadoesdev/custom-events-components

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
src/lib/settings.ts (1)

43-51: ⚠️ Potential issue | 🟠 Major

Silent failure when setPluginData permission is missing.

saveSetting resolves successfully when the user lacks setPluginData permission, but nothing is persisted. Every caller in App.tsx (handleSettingChange, handleClientIdChange, handleDebugChange, handleDisabledChange, saveApiUrl, the pattern/sampling handlers, etc.) then updates local React state and calls updateScript(), which reads stale values back from framer.getPluginData. Net effect: the UI appears to change, the Pause toggle visually flips, but the injected <script> tag is not re-synced — exactly the opposite of what the PR's permission gating is trying to achieve.

Consider returning a success flag (or throwing) so callers can short-circuit and notify the user, e.g.:

♻️ Proposed fix
 export async function saveSetting(
   id: string,
   value: string | null
-): Promise<void> {
-  if (!framer.isAllowedTo("setPluginData")) {
-    return;
-  }
-  await framer.setPluginData(`${SETTING_PREFIX}${id}`, value);
+): Promise<boolean> {
+  if (!framer.isAllowedTo("setPluginData")) {
+    return false;
+  }
+  await framer.setPluginData(`${SETTING_PREFIX}${id}`, value);
+  return true;
 }

Callers in App.tsx should then skip the local state/updateScript() update (or surface a framer.notify warning) when persistence didn't happen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/settings.ts` around lines 43 - 51, saveSetting currently silently
returns when framer.isAllowedTo("setPluginData") is false which causes callers
to proceed as if persistence succeeded; change saveSetting(id, value) to
indicate failure (either throw an Error or return a boolean) so callers can
short-circuit persistence-dependent updates: inside saveSetting (referencing
framer.isAllowedTo, framer.setPluginData, and SETTING_PREFIX) return false or
throw when permission is missing and return true after successful
framer.setPluginData; then update callers in App.tsx (handleSettingChange,
handleClientIdChange, handleDebugChange, handleDisabledChange, saveApiUrl and
the pattern/sampling handlers) to check the result and skip local
state/updateScript() or surface a framer.notify warning when saveSetting failed.
src/App.tsx (1)

289-320: ⚠️ Potential issue | 🟡 Minor

Pattern/sampling inputs persist and regenerate the script on every keystroke.

handleSamplingRateChange, handleSkipPatternsChange, and handleMaskPatternsChange call saveSetting + updateScript() on every onChange. Two concerns:

  1. Intermediate invalid JSON reaches the published script. While a user types ["/admin" the script is rewritten with that partial value as a data-skip-patterns attribute (see src/lib/script.ts line 86-91). Only the last keystroke is valid.
  2. framer.setCustomCode + framer.setPluginData run on every keystroke, where a debounce or an onBlur save (like the pattern you already use for saveDashboardUrl / saveApiUrl at lines 246-265) would be more efficient and avoid flooding Framer's custom-code subscribers.

Recommend switching these three to the same onBlur + local-state pattern used by the URL fields, and optionally validating the JSON on blur before persisting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 289 - 320, The three handlers
handleSamplingRateChange, handleSkipPatternsChange, and handleMaskPatternsChange
currently persist on every keystroke; change them to update only local component
state on input change (call setSamplingRate/setSkipPatterns/setMaskPatterns) and
move saveSetting + updateScript calls into corresponding onBlur handlers so
persistence and framer.setCustomCode/framer.setPluginData only run when the
field loses focus (and only if isInstalled). Additionally, onBlur validate JSON
for skipPatterns and maskPatterns (and coerce/validate samplingRate) before
calling saveSetting; if invalid, do not persist and surface an error. Keep
saveSetting and updateScript usage but invoke them from the new onBlur handlers
and ensure they receive the validated value (value || null).
🧹 Nitpick comments (4)
src/components/ui/label.tsx (1)

22-25: Minor: wrapping arbitrary ReactNode in <p> may produce invalid HTML.

tooltip is typed as ReactNode, so a caller could pass a block element (e.g. <div>, <ul>) and end up nested inside a <p>, which React will warn about and browsers auto-close. All current call sites in App.tsx pass strings, so this is latent rather than active.

♻️ Proposed fix
-        <TooltipContent className="tooltip-large" side="top">
-          <p>{tooltip}</p>
-        </TooltipContent>
+        <TooltipContent className="tooltip-large" side="top">
+          {typeof tooltip === "string" ? <p>{tooltip}</p> : tooltip}
+        </TooltipContent>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/label.tsx` around lines 22 - 25, The TooltipContent
currently wraps the ReactNode prop tooltip in a <p>, which can produce invalid
HTML if callers pass block elements; update the Label component so
TooltipContent renders tooltip directly (i.e., remove the hardcoded <p>), or
alternatively conditionally wrap only when typeof tooltip === 'string' (or
React.isValidElement check) so blocks like <div> or <ul> are not nested inside a
paragraph; update the JSX around TooltipContent (and keep
TooltipTrigger/InfoIcon usage unchanged) and adjust typings/comments if needed.
src/lib/script.ts (1)

21-31: isValidHttpUrl("") returns true — document or rename.

Returning true for the empty string is intentional (empty = use default), but the name implies pure validation. Callers outside App.tsx's blur handlers could be surprised. A short JSDoc note or rename to isValidHttpUrlOrEmpty would make the contract explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/script.ts` around lines 21 - 31, The function isValidHttpUrl
currently returns true for the empty string which is intentional but mismatches
its name; either add a JSDoc comment on isValidHttpUrl clarifying that empty
string is treated as valid (meaning "use default"), or rename the function to
isValidHttpUrlOrEmpty and update all call sites (e.g., the blur handlers that
rely on this behavior) to use the new name; ensure the chosen change is applied
consistently and the JSDoc (if added) states the exact contract: empty string
allowed, otherwise must be http/https.
src/lib/overrides.ts (1)

5-145: Consider moving COMPONENT_FILE_CONTENT to a separate asset.

Embedding ~140 lines of TSX as a template literal inside a .ts file loses type-checking, lint, and editor tooling on arguably the most user-facing code in the plugin — and a bad edit here silently ships to every user's Databuddy.tsx. Two cleaner options:

  • Keep the code in a real src/lib/component-template.tsx file and import it as a raw string via Vite's ?raw import (e.g., import componentContent from './component-template.tsx?raw').
  • Or keep it inline but add a unit test that at minimum runs the string through a TS parser to catch regressions before shipping.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/overrides.ts` around lines 5 - 145, COMPONENT_FILE_CONTENT currently
embeds ~140 lines of TSX as a template string which loses type/lint tooling;
move that TSX into a real asset and then replace the inline string with an
import of the raw text (assigning it back to COMPONENT_FILE_CONTENT) via your
bundler's raw import (e.g., import componentContent from
'./component-template.tsx?raw' and set COMPONENT_FILE_CONTENT =
componentContent), or if you prefer to keep it inline add a unit test that
parses COMPONENT_FILE_CONTENT with the TypeScript parser to catch syntax/type
regressions before release; update any references expecting the original string
(COMPONENT_FILE_CONTENT) so TrackClick / TrackSubmit / TrackView consumers
continue to work.
src/App.css (1)

121-143: Prevent component rows from overflowing in narrow plugin panels.

The list row uses display: flex, but neither text side sets min-width: 0; long component names/descriptions can force the row wider than the Framer plugin sidebar instead of truncating or wrapping.

Proposed CSS adjustment
 .components-list > li {
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: 12px;
   padding: 8px 12px;
   font-size: 12px;
 }
 
 .components-name {
   color: var(--framer-color-text);
   font-weight: 500;
+  min-width: 0;
 }
 
 .components-desc {
   color: var(--framer-color-text-tertiary);
   font-size: 11px;
   text-align: right;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 121 - 143, The flex children in .components-list >
li (.components-name and .components-desc) lack min-width constraints, allowing
long text to force the row wider; update the CSS for .components-name and
.components-desc to set min-width: 0 and apply overflow handling (overflow:
hidden; text-overflow: ellipsis; white-space: nowrap or allow wrapping as
desired) so long names/descriptions truncate instead of overflowing the plugin
panel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App.tsx`:
- Around line 188-201: The menu currently shows "Open Tracking Components" but
wired to handleInstallComponents which calls installComponents() and overwrites
Databuddy.tsx; change the menu logic so when componentsInstalled is true it uses
a new open-only handler (e.g., handleOpenComponents) that only opens/navigates
to the existing Databuddy.tsx without calling installComponents() or
existing.setFileContent, and add a separate menu entry "Reinstall/Update
Tracking Components" that calls handleInstallComponents to perform the
overwrite; update references to componentsInstalled, handleInstallComponents,
handleRemoveComponents, installComponents(), COMPONENT_FILE_CONTENT and
Databuddy.tsx to ensure the new handler only opens and never writes the file.

In `@src/lib/overrides.ts`:
- Around line 88-114: In TrackView, the fired ref is never reset so changing
props.eventName or props.properties won't allow the observer to fire again;
update the effect so that when the identity-defining props (props.eventName and
props.properties, and threshold if desired) change you reset fired.current =
false before creating the IntersectionObserver (or, if the intended behavior is
“fire once per mount”, remove those props from the effect deps and read them via
refs instead) to ensure the observer can fire for the new configuration.
- Around line 176-182: The removeComponents function can throw for view-only
users because it calls existing.remove() without checking permissions; update
removeComponents to first call framer.isAllowedTo("CodeFile.remove") and
early-return (resolve/return void) when false, or alternatively add a permission
gate in App.tsx using useIsAllowedTo("CodeFile.remove") to disable the Remove
button and menu path (the latter mirrors the existing
canCreateComponents/installComponents pattern); ensure references to
findComponentFile and handleRemoveComponents still work and surface an
informative result or no-op instead of attempting existing.remove() when not
allowed.

In `@src/lib/script.ts`:
- Around line 61-98: The interpolated attribute values (clientId, customApiUrl,
skipPatterns, maskPatterns) are not escaped and can break the <script> tag or
allow attribute injection; add a small helper (e.g., escapeHtmlAttr) and call it
before interpolation: escape values used inside double-quoted attributes
(clientId, customApiUrl, any values added to attrString) by replacing " with
&quot; (and & with &amp;), and escape values used inside single-quoted
attributes (skipPatterns, maskPatterns) by replacing ' with &#39; (and & with
&amp;); update the template in the return expression to use the escaped versions
(refer to clientId, customApiUrl, skipPatterns, maskPatterns) and also consider
validating skipPatterns/maskPatterns as JSON at save time (same logic you use in
App.tsx) to avoid shipping malformed JSON.

---

Outside diff comments:
In `@src/App.tsx`:
- Around line 289-320: The three handlers handleSamplingRateChange,
handleSkipPatternsChange, and handleMaskPatternsChange currently persist on
every keystroke; change them to update only local component state on input
change (call setSamplingRate/setSkipPatterns/setMaskPatterns) and move
saveSetting + updateScript calls into corresponding onBlur handlers so
persistence and framer.setCustomCode/framer.setPluginData only run when the
field loses focus (and only if isInstalled). Additionally, onBlur validate JSON
for skipPatterns and maskPatterns (and coerce/validate samplingRate) before
calling saveSetting; if invalid, do not persist and surface an error. Keep
saveSetting and updateScript usage but invoke them from the new onBlur handlers
and ensure they receive the validated value (value || null).

In `@src/lib/settings.ts`:
- Around line 43-51: saveSetting currently silently returns when
framer.isAllowedTo("setPluginData") is false which causes callers to proceed as
if persistence succeeded; change saveSetting(id, value) to indicate failure
(either throw an Error or return a boolean) so callers can short-circuit
persistence-dependent updates: inside saveSetting (referencing
framer.isAllowedTo, framer.setPluginData, and SETTING_PREFIX) return false or
throw when permission is missing and return true after successful
framer.setPluginData; then update callers in App.tsx (handleSettingChange,
handleClientIdChange, handleDebugChange, handleDisabledChange, saveApiUrl and
the pattern/sampling handlers) to check the result and skip local
state/updateScript() or surface a framer.notify warning when saveSetting failed.

---

Nitpick comments:
In `@src/App.css`:
- Around line 121-143: The flex children in .components-list > li
(.components-name and .components-desc) lack min-width constraints, allowing
long text to force the row wider; update the CSS for .components-name and
.components-desc to set min-width: 0 and apply overflow handling (overflow:
hidden; text-overflow: ellipsis; white-space: nowrap or allow wrapping as
desired) so long names/descriptions truncate instead of overflowing the plugin
panel.

In `@src/components/ui/label.tsx`:
- Around line 22-25: The TooltipContent currently wraps the ReactNode prop
tooltip in a <p>, which can produce invalid HTML if callers pass block elements;
update the Label component so TooltipContent renders tooltip directly (i.e.,
remove the hardcoded <p>), or alternatively conditionally wrap only when typeof
tooltip === 'string' (or React.isValidElement check) so blocks like <div> or
<ul> are not nested inside a paragraph; update the JSX around TooltipContent
(and keep TooltipTrigger/InfoIcon usage unchanged) and adjust typings/comments
if needed.

In `@src/lib/overrides.ts`:
- Around line 5-145: COMPONENT_FILE_CONTENT currently embeds ~140 lines of TSX
as a template string which loses type/lint tooling; move that TSX into a real
asset and then replace the inline string with an import of the raw text
(assigning it back to COMPONENT_FILE_CONTENT) via your bundler's raw import
(e.g., import componentContent from './component-template.tsx?raw' and set
COMPONENT_FILE_CONTENT = componentContent), or if you prefer to keep it inline
add a unit test that parses COMPONENT_FILE_CONTENT with the TypeScript parser to
catch syntax/type regressions before release; update any references expecting
the original string (COMPONENT_FILE_CONTENT) so TrackClick / TrackSubmit /
TrackView consumers continue to work.

In `@src/lib/script.ts`:
- Around line 21-31: The function isValidHttpUrl currently returns true for the
empty string which is intentional but mismatches its name; either add a JSDoc
comment on isValidHttpUrl clarifying that empty string is treated as valid
(meaning "use default"), or rename the function to isValidHttpUrlOrEmpty and
update all call sites (e.g., the blur handlers that rely on this behavior) to
use the new name; ensure the chosen change is applied consistently and the JSDoc
(if added) states the exact contract: empty string allowed, otherwise must be
http/https.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0eaa3eaf-6370-4bc4-b9dc-98a35be3137a

📥 Commits

Reviewing files that changed from the base of the PR and between 383b0a2 and 90cad8c.

📒 Files selected for processing (9)
  • src/App.css
  • src/App.tsx
  • src/components/ui/label.tsx
  • src/framer.css
  • src/hooks/use-overrides.ts
  • src/lib/overrides.ts
  • src/lib/script.ts
  • src/lib/settings.ts
  • src/util/integration.ts

Comment thread src/App.tsx
Comment on lines +188 to +201
{
label: componentsInstalled
? "Open Tracking Components"
: "Install Tracking Components",
onAction: handleInstallComponents,
},
...(componentsInstalled
? [
{
label: "Remove Tracking Components",
onAction: handleRemoveComponents,
} as const,
]
: []),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"Open Tracking Components" menu item silently overwrites user edits.

When componentsInstalled is true, the menu shows "Open Tracking Components" but its onAction is handleInstallComponentsinstallComponents(), which calls existing.setFileContent(COMPONENT_FILE_CONTENT) before navigating (see src/lib/overrides.ts lines 163-166). Any customization a user made to Databuddy.tsx is lost the moment they choose "Open". Users reasonably expect "Open" to only navigate.

Suggest splitting into an open-only action when the file already exists:

♻️ Proposed fix
       {
         label: componentsInstalled
           ? "Open Tracking Components"
           : "Install Tracking Components",
-        onAction: handleInstallComponents,
+        onAction: componentsInstalled
+          ? async () => {
+              if (componentFile) await componentFile.navigateTo();
+            }
+          : handleInstallComponents,
       },

You can keep a separate "Reinstall/Update Tracking Components" entry for the overwrite path.

Comment thread src/lib/components.ts
Comment on lines +176 to +182
export async function removeComponents(): Promise<void> {
const files = await framer.getCodeFiles();
const existing = findComponentFile(files);
if (existing) {
await existing.remove();
}
}

Copy link
Copy Markdown

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:

#!/bin/bash
# Confirm the exact permission identifier for removing a code file in framer-plugin v3.0.0
rg -nP --type=ts -C2 "isAllowedTo\(" 
fd -i 'framer-plugin' node_modules 2>/dev/null | head
rg -nP -C2 "removeCodeFile|'createCodeFile'|\"createCodeFile\""

Repository: databuddy-analytics/databuddy-framer

Length of output: 646


🏁 Script executed:

#!/bin/bash
# Find handleRemoveComponents and understand the flow
rg -nP "handleRemoveComponents" --type=ts -A5 -B2

Repository: databuddy-analytics/databuddy-framer

Length of output: 1492


🏁 Script executed:

#!/bin/bash
# Find where removeComponents is called and how the remove button is implemented
rg -nP "removeComponents|Remove button|remove.*button" --type=ts -A3 -B2

Repository: databuddy-analytics/databuddy-framer

Length of output: 1041


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or type definitions for Framer plugin permissions
rg -nP "removeCodeFile|remove.*Code.*File" --type=ts
find . -name "*.d.ts" -o -name "framer*.ts" | head -20

Repository: databuddy-analytics/databuddy-framer

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Get more context on canCreateComponents usage and see what other permissions exist
rg -nP "useIsAllowedTo\(" --type=ts -A1 | head -30

Repository: databuddy-analytics/databuddy-framer

Length of output: 303


🏁 Script executed:

#!/bin/bash
# Check for any framer API calls that might document available permissions
rg -nP "framer\.(isAllowedTo|getCodeFiles|notify)" --type=ts -B2 -A2

Repository: databuddy-analytics/databuddy-framer

Length of output: 3897


🏁 Script executed:

#!/bin/bash
# Look for error handling patterns and any documentation in comments
rg -nP "remove\(\)|Could not remove" --type=ts -B3 -A3

Repository: databuddy-analytics/databuddy-framer

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Check package.json or any type definitions for framer-plugin version and permissions
cat package.json 2>/dev/null | grep -A2 -B2 "framer"
head -50 src/vite-env.d.ts

Repository: databuddy-analytics/databuddy-framer

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Search for any comments or documentation about available framer permissions
rg -nP "permission|Permission|PERMISSION" --type=ts -i | grep -i "remove\|create\|code" | head -20

Repository: databuddy-analytics/databuddy-framer

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Check if there's any README or documentation about Framer permissions
find . -maxdepth 3 -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l "permission\|removeCodeFile" 2>/dev/null | head -5

Repository: databuddy-analytics/databuddy-framer

Length of output: 62


🌐 Web query:

Framer plugin v3 permissions API removeCodeFile

💡 Result:

In Framer Plugins API v3, the remove method on a CodeFile instance (accessed via framer.getCodeFile or createCodeFile) removes the code file from the project: await codeFile.remove;. This method is likely protected by permissions, as it modifies the project. Plugins must check user permissions using the Permissions API before calling it to avoid FramerPluginError. To check permissions for CodeFile.remove, use framer.isAllowedTo("CodeFile.remove"), which returns a boolean indicating if the user can execute it. For React plugins, use the useIsAllowedTo("CodeFile.remove") hook. For dynamic checks, framer.subscribeToIsAllowedTo("CodeFile.remove", callback). Example usage: if (framer.isAllowedTo("CodeFile.remove")) { await codeFile.remove; } else { // Handle insufficient permissions, e.g., show message } Permissions depend on the user's project role (e.g., Design or Content access). No explicit confirmation that CodeFile.remove is protected, but follows pattern for destructive class methods like "CollectionItem.remove". Methods starting with "get" are typically always allowed. If permissions are insufficient, a toast shows required access. Official docs: CodeFile.remove (framer.com/developers/reference/plugins-code-file-remove), Permissions guide (framer.com/developers/plugins-permissions).

Citations:


🏁 Script executed:

#!/bin/bash
# Verify the codebase isn't already using CodeFile.remove permission
rg -nP "CodeFile\." --type=ts
rg -nP "CodeFile\.remove" --type=ts

Repository: databuddy-analytics/databuddy-framer

Length of output: 62


removeComponents does not gate on CodeFile.remove permission.

installComponents is protected at the App layer via canCreateComponents = useIsAllowedTo("createCodeFile"), but the remove button and menu simply call handleRemoveComponents without any equivalent check. If a view-only collaborator ends up with this button enabled (e.g. the Framer menu path, which never checks permissions), existing.remove() will throw and only show an "Could not remove file" toast.

Recommend either:

  • Adding a useIsAllowedTo("CodeFile.remove") check in App.tsx mirroring canCreateComponents, disabling the menu entry and Remove button accordingly, or
  • Early-returning from removeComponents() with an informative result when framer.isAllowedTo("CodeFile.remove") is false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/overrides.ts` around lines 176 - 182, The removeComponents function
can throw for view-only users because it calls existing.remove() without
checking permissions; update removeComponents to first call
framer.isAllowedTo("CodeFile.remove") and early-return (resolve/return void)
when false, or alternatively add a permission gate in App.tsx using
useIsAllowedTo("CodeFile.remove") to disable the Remove button and menu path
(the latter mirrors the existing canCreateComponents/installComponents pattern);
ensure references to findComponentFile and handleRemoveComponents still work and
surface an informative result or no-op instead of attempting existing.remove()
when not allowed.

Comment thread src/lib/script.ts
…ename overrides → components

- Escape every value interpolated into the injected <script> tag.
  Skip/mask patterns previously used single-quoted attributes to permit
  JSON's double quotes; a literal single quote in user input would break
  out and inject arbitrary HTML/JS into the published site.
- Coerce samplingRate to a number and only emit data-sampling-rate when
  it parses to a finite value in [0, 1). Fixes "1.0" / "1.00" leaking
  through the previous string compare against "1".
- Validate customApiUrl with isValidHttpUrl before injection.
- Rename src/lib/overrides.ts → src/lib/components.ts and
  src/hooks/use-overrides.ts → src/hooks/use-component-file.ts to match
  the implementation (Code Components, not Code Overrides).
- Hold clientId in a ref inside the Open Dashboard menu action so the
  menu effect no longer rebinds on every keystroke.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/App.tsx (1)

286-317: ⚠️ Potential issue | 🟠 Major

Filtering inputs write to Framer custom code on every keystroke.

handleSamplingRateChange, handleSkipPatternsChange, and handleMaskPatternsChange each call saveSetting and, when installed, updateScript() — which ends up in framer.setCustomCode({ html, location: "headEnd" }) on every character typed. That's both a lot of unnecessary project writes and produces transient invalid values for skipPatterns/maskPatterns (e.g. while the user is mid-typing a JSON array, every intermediate state like [ or ["/adm is persisted and shipped). You already adopted onBlur for the dashboard/API URL inputs (lines 536, 553) for the same reason — apply the same pattern here, or debounce.

♻️ Suggested pattern (onBlur persistence for pattern inputs)
-  const handleSkipPatternsChange = useCallback(
-    async (value: string) => {
-      setSkipPatterns(value);
-      await saveSetting("skipPatterns", value || null);
-      if (isInstalled) {
-        await updateScript();
-      }
-    },
-    [isInstalled]
-  );
+  const saveSkipPatterns = useCallback(async () => {
+    await saveSetting("skipPatterns", skipPatterns || null);
+    if (isInstalled) await updateScript();
+  }, [skipPatterns, isInstalled]);

…then wire onChange={(e) => setSkipPatterns(e.target.value)} + onBlur={saveSkipPatterns} on the input (same for maskPatterns, and optionally samplingRate).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 286 - 317, The three handlers
(handleSamplingRateChange, handleSkipPatternsChange, handleMaskPatternsChange)
currently call saveSetting and updateScript on every keystroke; change them to
only update local state (use setSamplingRate, setSkipPatterns, setMaskPatterns)
on change, and move the saveSetting("...", value) + conditional updateScript()
calls into separate onBlur handlers (e.g., saveSamplingRate, saveSkipPatterns,
saveMaskPatterns) or implement a debounce that persists after a short idle;
ensure the new onBlur/debounced functions call saveSetting and, if isInstalled,
updateScript so framer.setCustomCode is not invoked on every character.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/components.ts`:
- Around line 88-114: TrackView's wrapper uses style {{ display: "contents" }},
which yields no box so IntersectionObserver.observe(ref.current) never fires;
fix by giving the observed element a real layout box or observing a real child:
either remove display: "contents" and use a block-level wrapper (update the div
in TrackView) so ref.current has a bounding box, OR instead attach the
ref/observer to the first rendered child element (use
React.Children.toArray(props.children)[0] and cloneElement to forward the ref or
find the child's DOM node) and observe that child; ensure fired/ref/observer
logic in TrackView remains the same and keep
props.threshold/props.eventName/props.properties in the effect dependencies.

In `@src/lib/script.ts`:
- Around line 83-107: The includeSampling condition currently excludes 1.0, so a
user-entered samplingNumber of exactly 1 is saved but never emitted; update the
logic around samplingNumber/includeSampling/samplingAttr so values in the full
[0, 1] range are accepted (i.e., use samplingNumber <= 1 instead of < 1) and
ensure samplingAttr is set when samplingNumber === 1.0 (so
attr("data-sampling-rate", String(samplingNumber)) is emitted for 1.0 as well),
referencing the variables samplingRaw, samplingNumber, includeSampling, and
samplingAttr when making the change.

---

Outside diff comments:
In `@src/App.tsx`:
- Around line 286-317: The three handlers (handleSamplingRateChange,
handleSkipPatternsChange, handleMaskPatternsChange) currently call saveSetting
and updateScript on every keystroke; change them to only update local state (use
setSamplingRate, setSkipPatterns, setMaskPatterns) on change, and move the
saveSetting("...", value) + conditional updateScript() calls into separate
onBlur handlers (e.g., saveSamplingRate, saveSkipPatterns, saveMaskPatterns) or
implement a debounce that persists after a short idle; ensure the new
onBlur/debounced functions call saveSetting and, if isInstalled, updateScript so
framer.setCustomCode is not invoked on every character.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ce42d22-c245-4a60-8da9-23aa09151c22

📥 Commits

Reviewing files that changed from the base of the PR and between 90cad8c and c0c7794.

📒 Files selected for processing (4)
  • src/App.tsx
  • src/hooks/use-component-file.ts
  • src/lib/components.ts
  • src/lib/script.ts

Comment thread src/lib/components.ts
Comment on lines +88 to +114
export function TrackView(props) {
const ref = useRef(null)
const fired = useRef(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !fired.current) {
fired.current = true
fire(props.eventName, props.properties)
}
},
{ threshold: props.threshold ?? 0.5 }
)
observer.observe(el)
return () => observer.disconnect()
}, [props.eventName, props.properties, props.threshold])
return (
<div
ref={ref}
style={{ width: "100%", height: "100%", display: "contents" }}
>
{props.children}
</div>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does IntersectionObserver reliably report isIntersecting for an element with display: contents in Chrome, Safari, and Firefox?

💡 Result:

No, IntersectionObserver does not reliably report isIntersecting for an element with display: contents in Chrome, Safari, or Firefox. Elements styled with display: contents generate no layout box, so element.getBoundingClientRect returns a zero-area rectangle (width: 0, height: 0). IntersectionObserver uses this boundingClientRect to compute intersections per the specification; a zero-area targetRect results in intersectionRatio of 0 or 1 only if edge-adjacent, but typically no meaningful intersection callbacks occur, and isIntersecting remains false even if children are visible. This is by design, consistent across browsers, with no browser-specific bugs found in recent sources (up to 2026). Workarounds include observing child elements instead.

Citations:


display: contents on the observed element breaks IntersectionObserver.

The wrapper that carries ref for the observer uses style={{ width: "100%", height: "100%", display: "contents" }}. An element with display: contents generates no principal box—it produces a zero-area bounding rectangle—so IntersectionObserver.observe(el) on such an element will not fire, regardless of whether the rendered children are in view. This is spec-compliant behavior across all browsers (Chrome, Safari, Firefox) and directly defeats the purpose of TrackView.

Two reasonable options:

  • Remove display: contents from the wrapper and let it render with display: block, or
  • Observe the first rendered child element instead of the contents-only wrapper.
♻️ Minimal patch — give TrackView its own box
-    return (
-        <div
-            ref={ref}
-            style={{ width: "100%", height: "100%", display: "contents" }}
-        >
-            {props.children}
-        </div>
-    )
+    return (
+        <div
+            ref={ref}
+            style={{ width: "100%", height: "100%" }}
+        >
+            {props.children}
+        </div>
+    )
📝 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
export function TrackView(props) {
const ref = useRef(null)
const fired = useRef(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !fired.current) {
fired.current = true
fire(props.eventName, props.properties)
}
},
{ threshold: props.threshold ?? 0.5 }
)
observer.observe(el)
return () => observer.disconnect()
}, [props.eventName, props.properties, props.threshold])
return (
<div
ref={ref}
style={{ width: "100%", height: "100%", display: "contents" }}
>
{props.children}
</div>
)
}
export function TrackView(props) {
const ref = useRef(null)
const fired = useRef(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !fired.current) {
fired.current = true
fire(props.eventName, props.properties)
}
},
{ threshold: props.threshold ?? 0.5 }
)
observer.observe(el)
return () => observer.disconnect()
}, [props.eventName, props.properties, props.threshold])
return (
<div
ref={ref}
style={{ width: "100%", height: "100%" }}
>
{props.children}
</div>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components.ts` around lines 88 - 114, TrackView's wrapper uses style
{{ display: "contents" }}, which yields no box so
IntersectionObserver.observe(ref.current) never fires; fix by giving the
observed element a real layout box or observing a real child: either remove
display: "contents" and use a block-level wrapper (update the div in TrackView)
so ref.current has a bounding box, OR instead attach the ref/observer to the
first rendered child element (use React.Children.toArray(props.children)[0] and
cloneElement to forward the ref or find the child's DOM node) and observe that
child; ensure fired/ref/observer logic in TrackView remains the same and keep
props.threshold/props.eventName/props.properties in the effect dependencies.

Comment thread src/lib/script.ts
Comment on lines +83 to +107
const samplingRaw = await framer.getPluginData(
`${SETTING_PREFIX}samplingRate`
);
const samplingNumber = samplingRaw ? Number(samplingRaw) : Number.NaN;
const includeSampling =
Number.isFinite(samplingNumber) &&
samplingNumber >= 0 &&
samplingNumber < 1;

const skipPatterns = await framer.getPluginData(
`${SETTING_PREFIX}skipPatterns`
);
const maskPatterns = await framer.getPluginData(
`${SETTING_PREFIX}maskPatterns`
);

const apiUrlAttr =
customApiUrl && isValidHttpUrl(customApiUrl)
? attr("data-api-url", customApiUrl)
: "";
const disabledAttr =
isDisabled === "true" ? `\n data-disabled="true"` : "";
const samplingAttr = includeSampling
? attr("data-sampling-rate", String(samplingNumber))
: "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sampling-rate upper bound excludes 1.0, but the UI accepts it.

includeSampling requires samplingNumber < 1, so a user entering exactly 1.0 (which the <input type="number" max="1"> at src/App.tsx lines 411-421 accepts and which is the natural "track 100%" value) will silently result in no data-sampling-rate attribute. That happens to be equivalent to the SDK default, but the UX is surprising — the value is saved yet never emitted.

Either (a) accept <= 1 and emit, letting the SDK ignore a 1.0, or (b) clamp/warn in the UI so 1.0 can't be entered as a "sampling" value. Option (a) is the least surprising:

♻️ Proposed tweak
-  const includeSampling =
-    Number.isFinite(samplingNumber) &&
-    samplingNumber >= 0 &&
-    samplingNumber < 1;
+  const includeSampling =
+    Number.isFinite(samplingNumber) &&
+    samplingNumber >= 0 &&
+    samplingNumber <= 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/script.ts` around lines 83 - 107, The includeSampling condition
currently excludes 1.0, so a user-entered samplingNumber of exactly 1 is saved
but never emitted; update the logic around
samplingNumber/includeSampling/samplingAttr so values in the full [0, 1] range
are accepted (i.e., use samplingNumber <= 1 instead of < 1) and ensure
samplingAttr is set when samplingNumber === 1.0 (so attr("data-sampling-rate",
String(samplingNumber)) is emitted for 1.0 as well), referencing the variables
samplingRaw, samplingNumber, includeSampling, and samplingAttr when making the
change.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 9 files

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