Skip to content

feat: support analytics for custom code section#1262

Open
jhelenek wants to merge 1 commit into
mainfrom
analytics-branch
Open

feat: support analytics for custom code section#1262
jhelenek wants to merge 1 commit into
mainfrom
analytics-branch

Conversation

@jhelenek

@jhelenek jhelenek commented Jul 9, 2026

Copy link
Copy Markdown

This PR supports analytics for custom code section. It adds the Analytics track method to the window so that inner-components are able to reference it. Each analytics is registered by componentId so multiple custom code components can coexist without overwriting each other's handlers.

TEST=manual
Created a dev-release with this change and set it up with a site. For that site's layout, create 3 separate custom code components, each with different analytic calls. Deployed changes and saw analytic events come in both on the network tab and in Snowflake.

Snowflake query:
select * from prod_analytics2.public.analytics_events where original_business_id = 4146665 order by event_timestamp desc;

Video:
https://drive.google.com/file/d/1SVUUV4njl_fKxF-ZfO3aDrCqPrm3PgAx/view?usp=drive_link

@jhelenek jhelenek added the create-dev-release Triggers dev release workflow label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Warning: Component files have been updated but no migrations have been added. See https://github.com/yext/visual-editor/blob/main/packages/visual-editor/src/components/migrations/README.md for more information.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

commit: cbe76b4

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change modifies CustomCodeSection.tsx to route injected user JavaScript through a new analytics bridge and script runner. A shared window.YextCustomCodeAnalytics object maintains per-componentId track handlers in a scopes map, exposing register/unregister/track operations with a default C_CUSTOM action and warnings for untracked scopes. A new CustomCodeAnalyticsBridgeAndScriptRunner component registers this bridge, replaces the previous script-tag injection effect, wraps the user script with a yextAnalytics.track helper forwarding to the shared bridge, and cleans up the script and registration on unmount. The runner is wired into CustomCodeSectionWrapper with componentId, containerRef, processedJavascript, and scriptTagId.

Sequence Diagram(s)

sequenceDiagram
  participant CustomCodeSectionWrapper
  participant CustomCodeAnalyticsBridgeAndScriptRunner
  participant WindowBridge as window.YextCustomCodeAnalytics
  participant ScriptTag as Injected Script

  CustomCodeSectionWrapper->>CustomCodeAnalyticsBridgeAndScriptRunner: render(componentId, processedJavascript, scriptTagId)
  CustomCodeAnalyticsBridgeAndScriptRunner->>WindowBridge: register(componentId, track handler)
  CustomCodeAnalyticsBridgeAndScriptRunner->>ScriptTag: remove existing script by scriptTagId
  CustomCodeAnalyticsBridgeAndScriptRunner->>ScriptTag: append new script with yextAnalytics.track wrapper
  ScriptTag->>WindowBridge: track(componentId, data)
  WindowBridge->>WindowBridge: lookup scope handler
  CustomCodeAnalyticsBridgeAndScriptRunner->>WindowBridge: unregister(componentId) on cleanup
  CustomCodeAnalyticsBridgeAndScriptRunner->>ScriptTag: remove script on cleanup
Loading

Possibly related PRs

  • yext/visual-editor#1141: Both PRs modify the same CustomCodeSection.tsx script-injection logic, including how the script tag is created/updated via scriptTagId.

Suggested reviewers: mkilpatrick, briantstephan, benlife5, asanehisa

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding analytics support for the custom code section.
Description check ✅ Passed The description matches the changeset and explains the analytics bridge and componentId-scoped handlers.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch analytics-branch

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/visual-editor/src/components/customCode/CustomCodeSection.tsx`:
- Around line 205-216: The injected user script in CustomCodeSection’s script
creation is wrapped in a block, which prevents global handler resolution for
inline callbacks like onclick="changeColor(this)". Update the script injection
around processedJavascript so user code runs at top level instead of inside a
block, and provide yextAnalytics through another mechanism in the same
CustomCodeSection flow without relying on block-scoped declarations.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f613126d-9a2d-4221-aba2-0222a8921d7f

📥 Commits

Reviewing files that changed from the base of the PR and between 99d3414 and cbe76b4.

📒 Files selected for processing (1)
  • packages/visual-editor/src/components/customCode/CustomCodeSection.tsx

Comment on lines +205 to +216
// Wrap user code in a block so each component gets a local yextAnalytics helper without leaking a global.
script.text = `
{
const yextAnalytics = {
track: (eventName, data) =>
window.YextCustomCodeAnalytics.track(${JSON.stringify(componentId)}, eventName, data),
};

${processedJavascript}
}
`;
containerRef.current.appendChild(script);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the test to confirm reliance on global-scope handler resolution
fd -t f 'CustomCodeSection.test.tsx' packages/visual-editor/src/components/customCode --exec sed -n '1,80p' {}

Repository: yext/visual-editor

Length of output: 2587


🏁 Script executed:

python3 - <<'PY'
import subprocess, textwrap, json, os, sys, tempfile

# Use node's vm to run code as a script in a fresh global-like context.
js = r"""
const vm = require('vm');

function run(src) {
  const sandbox = { console };
  vm.createContext(sandbox);
  try {
    vm.runInContext(src, sandbox);
  } catch (e) {
    return { error: String(e), sandbox };
  }
  return { sandbox };
}

for (const [name, src] of [
  ['direct function', `function changeColor() {}`],
  ['block function', `{ function changeColor() {} }`],
  ['block const func expr', `{ const changeColor = () => {} }`],
  ['block let class', `{ let changeColor = 1; class Foo {} }`],
]) {
  const res = run(src);
  const sandbox = res.sandbox || {};
  console.log('---', name);
  console.log('error:', res.error || null);
  console.log('has changeColor in sandbox:', Object.prototype.hasOwnProperty.call(sandbox, 'changeColor'));
  console.log('typeof sandbox.changeColor:', typeof sandbox.changeColor);
  console.log('has Foo in sandbox:', Object.prototype.hasOwnProperty.call(sandbox, 'Foo'));
  console.log('typeof sandbox.Foo:', typeof sandbox.Foo);
}
"""
subprocess.run(["node", "-e", js], check=False)
PY

Repository: yext/visual-editor

Length of output: 797


Wrapping user JS in a block breaks global handler resolution.
let/const/class declarations inside the injected script stay block-scoped, so inline handlers like onclick="changeColor(this)" will fail unless the handler is a function declaration. That still works only because of sloppy-mode Annex B semantics, so the current test can miss the regression. Keep the user script at top level and inject yextAnalytics another way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor/src/components/customCode/CustomCodeSection.tsx`
around lines 205 - 216, The injected user script in CustomCodeSection’s script
creation is wrapped in a block, which prevents global handler resolution for
inline callbacks like onclick="changeColor(this)". Update the script injection
around processedJavascript so user code runs at top level instead of inside a
block, and provide yextAnalytics through another mechanism in the same
CustomCodeSection flow without relying on block-scoped declarations.

@jhelenek jhelenek changed the title [DRAFT] Support analytics for custom code section feat: support analytics for custom code section Jul 9, 2026
@jhelenek jhelenek marked this pull request as ready for review July 9, 2026 20:13
Comment on lines 11 to +12

type AnalyticsTrackProps = Parameters<

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.

I think export type TrackProps exists in pages-components if that is easier to use?

ref={containerRef}
dangerouslySetInnerHTML={{ __html: processedHtml }}
/>
<CustomCodeAnalyticsBridgeAndScriptRunner

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does it not work to simply add track to the window like this?

const pagesAnalytics = useAnalytics();
  useEffect(() => {
    (window as any).YextCustomCodeAnalytics.track = (request) => {
      pagesAnalytics?.track(request);
    };

    return () => {
      delete (window as any).YextCustomCodeAnalytics.track;
    };
  }, [pagesAnalytics]);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Wouldn't window.YextCustomCodeAnalytics.track get overwritten if we had multiple custom code components?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

True. To avoid creating a separate instance per component you can probably add it in VisualEditorProvider which wraps everything and is used in every template.

@jhelenek jhelenek Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

if we move const pagesAnalytics = useAnalytics(); into VisualEditorProvider, custom-code events would not automatically be tagged with the customCodeSection scope, right?

Example tree:

<AnalyticsProvider>
  <VisualEditorProvider>
    <AnalyticsScopeProvider name="customCodeSection">
      <CustomCodeSection />
    </AnalyticsScopeProvider>
  </VisualEditorProvider>
</AnalyticsProvider>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create-dev-release Triggers dev release workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants