fix(hubspot-web): add id=hs-script-loader to prevent duplicate script injection#3833
fix(hubspot-web): add id=hs-script-loader to prevent duplicate script injection#3833harsh-joshi99 wants to merge 4 commits into
Conversation
… injection Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the HubSpot Web (Actions) browser destination to align with HubSpot’s own loader-script duplication detection by ensuring the injected tracking script uses the hs-script-loader DOM id, and adds a unit test for that behavior.
Changes:
- Passes
{ id: 'hs-script-loader' }when loading the HubSpot tracking script in destination initialization. - Adds a Jest test to assert the HubSpot script tag carries
id="hs-script-loader".
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/browser-destinations/destinations/hubspot-web/src/index.ts | Adds id: 'hs-script-loader' to the HubSpot loader script injection call. |
| packages/browser-destinations/destinations/hubspot-web/src/tests/index.test.ts | Adds a unit test asserting the injected HubSpot script has the expected id. |
…rrect src Addresses Copilot review: tighten the regression guard to select by script#hs-script-loader, assert exactly one element exists, and verify its src is the HubSpot loader URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
harsh-joshi99
left a comment
There was a problem hiding this comment.
Note
Self-Review
Posted by PR author (harsh-joshi99) — self-reviews cannot apply the APPROVED label. Intended verdict: Manual Review Needed plugin-review for the APPROVED label.
Code Review Personae Verdict: Manual Review Needed ⚠️
The one-line change (adding id='hs-script-loader' to the injected HubSpot tracking script) introduces no crashes, NPEs, resource leaks, races, or security issues, and the new test is correct and not flaky. All 3 bug-finder passes independently converged on a single design concern: loadScript dedups by src/status, not by id, so the fix is asymmetric and can produce a duplicate DOM id in multi-instance edge cases.
Policy scan clean.
📝 Deep Code Review (3 Passes) — 0 Critical, 0 High, 1 Medium, 2 Low 🟡
Consensus: all 3 independent passes flagged the same line (src/index.ts:95) and the same underlying mechanism — the runtime loadScript() dedups by s.src === src (load-script.ts:3) and short-circuits only when an existing matching script carries a Segment status attribute of 'loaded'/'loading'. The new id is never used for de-duplication. The single-instance / same-region path that STRATCONN-6721 targets is fully fixed (HubSpot's own embed guard does document.getElementById('hs-script-loader') and now finds Segment's script). The residual concern is asymmetry: (a) if a customer already has HubSpot's native embed snippet on the page (same src, no Segment status attr), Segment still falls through and injects a second <script id='hs-script-loader'>; (b) two HubSpot Web instances with differing src (EU + non-EU, or differing portalId) each inject an element with the same hardcoded id, producing a duplicate DOM id and a potentially wrong getElementById target. Both are narrow edge cases, not production crashes. The new test's toHaveLength(1) is sound — setup-after-env.ts resets document.head per test and the harness's empty <script> has no id, so no cross-test leakage or false match.
Findings
- 🟡 MEDIUM: Fix is asymmetric: loadScript dedups by src, not id (
packages/browser-destinations/destinations/hubspot-web/src/index.ts:95)
[Found by 3/3 passes] HubSpot's embed guard skips injection whendocument.getElementById('hs-script-loader')exists, which this PR now satisfies — good. But the runtime's own loadScript() (browser-destination-runtime/src/load-script.ts:3) dedups only bys.src === srcand returns early only when the found script has a Segment-setstatusattribute of 'loaded'/'loading'. Two consequences: (1) If the customer already has HubSpot's native embed snippet on the page (same srchttps://js.hs-scripts.com/{portalId}.js, id present but no Segmentstatusattr), loadScript falls past the dedup branch and injects a SECOND<script id='hs-script-loader'>— the opposite direction of the bug being fixed. (2) Two HubSpot Web instances whose src differs (EUjs-eu1.hs-scripts.com+ non-EUjs.hs-scripts.com, or two portalIds) each create an element carrying the same hardcoded id, yielding a duplicate DOM id and a getElementById target that resolves to whichever is first in document order. Neither is a crash; the common single-instance/same-region case (the STRATCONN-6721 target) is fully fixed.
Suggestion: Optionally mirror HubSpot's own guard before injecting — e.g.if (document.getElementById('hs-script-loader')) return window._hsqin initialize() — or have loadScript dedup by matching id when one is supplied. For multi-instance support, derive a per-instance id (include portalId/region) instead of a fixed string. If multi-portal-per-page is out of scope for this ticket, no change needed. - 🔵 LOW: Test asserts resolved IDL src; would break if URL became relative (
packages/browser-destinations/destinations/hubspot-web/src/__tests__/index.test.ts:50)
loaderScripts[0].srcreturns the IDL-resolved absolute URL. Because scriptPath is already absolute, resolution is an identity and the assertion holds correctly today. Noted only because it would silently break if the injected URL ever became relative.
Suggestion: Optional: assert viagetAttribute('src')to test the literal injected value rather than the resolved IDL value. - 🔵 LOW: Pre-existing: tag.parentElement throws when document has zero <script> tags (
packages/browser-destination-runtime/src/load-script.ts:45)
getElementsByTagName('script')[0]is undefined if the page has no script tags, sotag.parentElement?.insertBeforewould throw 'Cannot read properties of undefined'. NOT introduced or newly exercised by this PR (the id attribute changes nothing on this branch), and the test harness avoids it by appending an empty<script>in beforeEach. Flagged for completeness only; out of scope for this PR.
Suggestion: Out of scope. If hardened later, fall back to document.head when no script tag exists.
🛡️ Policy Compliance Review — No violations (3 policies evaluated)
Evaluated 3 policies across all review surfaces. No violations detected.
Review Stages Executed
- ✅ Deep Code Review (3 Passes)
- ✅ Policy Compliance Review
Generated by code-review-personae v0.8.7 | Deep Code Review (3 Passes), Policy Compliance Review
Addresses deepreview: getAttribute('src') checks the literal injected
value rather than the IDL-resolved absolute URL, so the assertion stays
meaningful even if the injected URL ever became relative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| test('loads hubspot script with id=hs-script-loader to prevent duplicate script injection', async () => { | ||
| const [event] = await hubspotDestination({ | ||
| portalId: '12345', | ||
| subscriptions | ||
| }) | ||
|
|
||
| await event.load(Context.system(), {} as Analytics) | ||
|
|
||
| const loaderScripts = Array.from(document.querySelectorAll<HTMLScriptElement>('script#hs-script-loader')) | ||
| expect(loaderScripts).toHaveLength(1) | ||
| expect(loaderScripts[0].getAttribute('src')).toBe('https://js.hs-scripts.com/12345.js') | ||
| }) |
Summary
Adds
{ id: 'hs-script-loader' }to the HubSpot tracking-scriptloadScriptcall in the hubspot-web browser destination. HubSpot's own embed uses thehs-script-loaderid to detect whether its script is already present; without it, the script can be injected twice.Fixes STRATCONN-6721.
Root cause
HubSpot's analytics script (loaded transitively by
js.hs-scripts.com/<portalId>.js) injects its region-specific tracking script through a guard keyed on the DOM idhs-script-loader. Confirmed verbatim in HubSpot's own code atjs.hs-analytics.net/analytics/<ts>/<portalId>.js:Segment was injecting
js.hs-scripts.comwithout that id, sodocument.getElementById('hs-script-loader')returned null, the guard didn't trip, and HubSpot injected a second (js-na1.hs-scripts.com) script. Setting the id on Segment's injected script makes the guard find it and skip the duplicate.References:
js.hs-scripts.com/<portalId>.js(~1.9 KB; loads the analytics script below)embedHubSpotScriptguard:js.hs-analytics.net/analytics/<ts>/<portalId>.jsChanges
src/index.ts: pass{ id: 'hs-script-loader' }todeps.loadScript(scriptPath, ...)src/__tests__/index.test.ts: add a test asserting the injected HubSpot script carriesid="hs-script-loader"Testing
Verified locally via the Action Tester with a real HubSpot portal ID, comparing
mainvs this branch.main)js.hs-scripts.com(id:'') + duplicatejs-na1.hs-scripts.com(id:'hs-script-loader')js.hs-scripts.com(id:'hs-script-loader'), no duplicateBefore — duplicate script injected (
main, unfixed):After — single script with
id=hs-script-loader(this branch):id.TypeError: Cannot read properties of undefined (reading 'html')inJSDOMEnvironment) — a pre-existing tooling mismatch (jest 30 vs ts-jest <28), unrelated to this change. CI will run the canonical suite.Deploy note
These changes have also been pushed directly to
stagingfor testing.🤖 Generated with Claude Code