Skip to content

feat(attribution): decorateEmbedUrl for booking and form iframes - #121

Open
Atroci wants to merge 1 commit into
mainfrom
feat/embed-decorate-url
Open

Atroci wants to merge 1 commit into
mainfrom
feat/embed-decorate-url

Conversation

@Atroci

@Atroci Atroci commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Acceptance: for an https host in "Embedded widget hosts", window.ClickTrail.decorateEmbedUrl(url) returns the URL with last-touch utm_* and the one selected click ID; for anything else (other host, www./sibling variant, http, signed URL, no consent, no data, non-string) it returns the input unchanged.

Why

Booking/form widgets that the site's own script loads in an iframe (e.g. a popup doing iframe.src = url; container.appendChild(iframe)) never go through link decoration, and rewriting src after insertion loads the iframe twice. Giving the site a function to call where it already sets src puts the attribution in the iframe URL from the first request, with no dependency on page load or a vendor "ready" message. Motivated by a client booking-widget QA where the handoff was lost on fast clicks.

What

  • Decorator.decorateEmbedUrl + public window.ClickTrail.decorateEmbedUrl(url).
  • Payload: last-touch utm_* (all-or-nothing: skipped if the URL already has any utm_*) + one click ID from Decorator.oneClickId (D3 selection, wbraid/gbraid own keys, Snapchat as ScCid). No visitor/session ID, history or token.
  • New setting embed_allowed_domains ("Embedded widget hosts", empty by default, sanitized by sanitize_domains_csv) and filter clicutcl_embed_allowed_domains (filter values normalized the same way). Exact host match: a third-party widget on a sibling subdomain must be listed.
  • Consent: same live rule as link decoration. Signed URLs skipped while link_skip_signed is on (default).
  • calendlyStamp now uses the shared oneClickId (same behavior).
  • Docs: INTEGRATIONS (new embed section incl. delayed-JS caveat), HOOKS-REFERENCE, SECURITY-PRIVACY, SETTINGS-AND-ADMIN, readmes, changelog (Unreleased).

Not in this PR (deferred, with triggers)

  • Automatic iframe src rewriting (MutationObserver): only if a site cannot edit its embed script and a double-load spike passes.
  • postMessage protocol into iframes: only once a vendor commits to the receiving side.
  • clicktrail-js parity.

Verification

  • node tools/qa/smoke.js: new validateEmbedUrlDecoration (selected click only, last-touch UTMs, site params kept, no UTM mixing, other host / www. / sibling / http / signed / javascript: / non-string unchanged, wbraid, ScCid, no consent, empty allowlist). Mutation checks (consent gate removed, suffix host match, wrong click ID) each fail the suite.
  • cmp-transition, consent-bridge, repeat-visit-scenarios pass.
  • PHPUnit 250/250, PHPCS clean on touched PHP (PHP 8.4 container).
  • Review pass run before commit; findings applied. One finding is pre-existing and out of scope: applyTouch merges lt_* per field, so a newer touch without utm_term keeps the older term. That affects every outbound path (links, forms, events) and should be its own fix.

🤖 Generated with Claude Code

Booking/form widgets that the site's own script loads in an iframe never
pass through link decoration, and rewriting an iframe src after insertion
loads it twice. window.ClickTrail.decorateEmbedUrl(url) returns the URL to
load instead, so the attribution is in the iframe URL from its first
request.

- Adds last-touch utm_* and one click ID (the D3 selection; wbraid/gbraid
  and ScCid under their own names) for https hosts in the new
  "Embedded widget hosts" setting (embed_allowed_domains, filter
  clicutcl_embed_allowed_domains, empty by default, exact host match).
- Returns the input unchanged without marketing consent, for other hosts,
  non-https or signed URLs (link_skip_signed), or with no attribution.
- Never overwrites site params; skips stored UTMs when the URL already has
  any utm_*, so one URL never mixes two campaigns.
- Decorator.oneClickId now backs both this and the Calendly stamp.
- Docs: INTEGRATIONS, HOOKS-REFERENCE, SECURITY-PRIVACY, SETTINGS-AND-ADMIN,
  readmes, changelog. Smoke checks in tools/qa/smoke.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T08:18:30.350686Z 5d4d73b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d4d73b949

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1314 to +1315
const bad = ["x-amz-signature", "signature", "sig", "token"];
return bad.some(k => qs.has(k) || qs.has(k.toUpperCase()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect mixed-case signature parameters

When an allowlisted embed uses a conventionally cased signature such as X-Amz-Signature or Signature, URLSearchParams' case-sensitive lookup misses it because only the lowercase and fully uppercase spellings are checked. decorateEmbedUrl() then appends attribution and reserializes the URL despite the default signed-URL protection, invalidating the signature and potentially preventing the iframe from loading; compare parameter names case-insensitively instead.

Useful? React with 👍 / 👎.

Comment on lines +1517 to +1519
const order = data.attribution_selected_click_id
? (selected ? [selected] : [])
: ['gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'ttclid', 'li_fat_id', 'twclid', 'dclid'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy Snapchat and Pinterest click IDs

For returning visitors whose attribution was stored before attribution_selected_click_id existed, the fallback omits the already-supported sccid and epik keys. After an upgrade, an embed handoff for a visitor acquired through Snapchat or Pinterest therefore sends its UTMs but no click ID until a new click is captured, even though both IDs are in D3_CLICK_ID_KEYS and the new API supports them; include them in the legacy fallback order.

Useful? React with 👍 / 👎.

Comment on lines +1556 to +1557
const click = this.oneClickId(data);
if (click) add(click[0] === 'sccid' ? 'ScCid' : click[0], click[1]); // Snapchat's own param name, as link decoration sends it

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid adding a second click ID to embed URLs

When the source widget URL already contains a click ID from another platform—for example gclid=OLD while the stored selection is fbclid=NEW—this adds the selected ID without checking the other supported click-ID keys, so the vendor receives two conflicting IDs despite the documented one-click-ID contract. Treat any existing supported click-ID parameter like the existing all-or-nothing UTM guard, while retaining the site's parameter unchanged.

Useful? React with 👍 / 👎.

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