Skip to content

feat: add LinkedIn Insight Tag#741

Open
zizzfizzix wants to merge 24 commits intonuxt:mainfrom
zizzfizzix:feat/script-registry/linkedin
Open

feat: add LinkedIn Insight Tag#741
zizzfizzix wants to merge 24 commits intonuxt:mainfrom
zizzfizzix:feat/script-registry/linkedin

Conversation

@zizzfizzix
Copy link
Copy Markdown
Contributor

@zizzfizzix zizzfizzix commented May 3, 2026

Summary

Adds the LinkedIn Insight Tag as a first-class registry entry under the ad category, alongside Bing UET / Meta / TikTok / Reddit / X / Snapchat pixels.

  • New composable useScriptLinkedInInsight() exposing a typed lintrk proxy with overloads for track and setUserData
  • Strict clientInit ordering matches the LinkedIn dedup doc requirement to set window._linkedin_event_id before the base code runs
  • Schema supports a single Partner ID or an array (multi-tenant push to _linkedin_data_partner_ids)
  • Optional eventId for page-load Conversions API deduplication
  • Optional enableAutoSpaTracking opt-in to virtual page-views via useScriptEventPage (mirrors the Matomo pattern); when enabled, window._wait_for_lintrk = true suppresses the script's built-in auto-page-view so each route fires exactly one lintrk('track')
  • First-party proxy domains: snap.licdn.com (script CDN) + px.ads.linkedin.com (covers /collect, /insight_tag_errors.gif, /attribution_trigger, /wa/)
  • bundle: true, proxy: true, partytown: { forwards: ['lintrk'] }, PRIVACY_FULL — same shape as every other ad pixel
  • LinkedIn-official light/dark logo, cleaned of page-instrumentation attributes

Design notes

  • No defaultConsent schema field. LinkedIn does not expose a lintrk('consent', …) command (verified against the published insight.min.js source — only track and setUserData cases exist in the dispatch switch). This matches X Pixel, Reddit Pixel, and Snapchat Pixel, which also have no defaultConsent. Consent gating is achieved today via useScript's existing trigger option. A wrapper-side load-gating consent helper (defaultConsent + consent.update()) is a defensible follow-up if symmetric ergonomics with Bing UET / Meta / TikTok are wanted.
  • SPA tracking is opt-in (default false). When off, the registry behaves like every other pixel — script auto-fires one page-view on load, SPA navigations are not tracked. When on, the composable feeds LinkedIn the same page-view signal it would get on a hard nav, using its own lintrk('track') (no synthetic image-pixel calls).
  • Multi-partner-ID support. id: string | string[]. Empty arrays are rejected at schema time so a misconfigured tag fails dev-mode validation rather than silently no-oping.

Test plan

  • pnpm test:run (unit + typecheck + e2e + nuxt-runtime) — passes
  • Playwright e2e suitetest/e2e/linkedin-insight.test.ts (bundled) + test/e2e/linkedin-insight-cdn.test.ts (CDN), each running the same 7-test suite from test/e2e/_linkedin-insight-suite.ts:
    • script tag points at /_scripts/assets/ (bundled) or snap.licdn.com (CDN)
    • partner ID + event ID globals are written by clientInit before the script loads
    • SPA navigation fires a /collect beacon when enableAutoSpaTracking: true
    • regression guard: initial page load fires exactly one canonical /collect (catches double-fire if _wait_for_lintrk suppression breaks)
    • SPA navigation fires no /collect when enableAutoSpaTracking: false
    • lintrk('track', { conversion_id }) includes conversionId in the URL
    • setUserData populates localStorage[li_hem] and the next page load transmits it inside the /wa/ POST body (decodes gzip+base64 fmt=g, base64 fmt=b, raw fmt=a)
  • Test fixtures share config via Nuxt layers — test/fixtures/linkedin-insight-cdn extends test/fixtures/linkedin-insight and only overrides bundle: false
  • Tests probe snap.licdn.com + px.ads.linkedin.com once in beforeAll; behavior tests skip automatically when egress is unavailable, while the two wiring tests (DOM + globals) run unconditionally
  • proxy-configs.test.ts and first-party.test.ts assertions extended with linkedinInsight
  • Type test expectTypeOf<Registry['linkedinInsight']>().not.toBeAny()
  • pnpm generate:types reproduces the committed registry-types.json
  • (For reviewers) Manual smoke test against a real LinkedIn Campaign Manager Insight Tag, confirming /collect requests reach LinkedIn under both direct and first-party-proxy modes

zizzfizzix added 15 commits May 2, 2026 10:23
…riants

LinkedIn ships separate light/dark SVG assets. Light is a solid blue
square with the "in" carved out (#0a66c2). Dark is a white rounded
square with the blue "in" rendered on top (#0077b5), so the mark is
readable on dark backgrounds. Strip page-instrumentation attributes
(data-supported-dps, focusable, class, role, aria-*, componentkey,
style, var(--svgDisplay*)) and split into the {light, dark} object
shape used by xPixel and carbonAds.
Replace em-dash separators with semicolons or sentence breaks per the
project's harlanzw/ai-deslop-no-em-dash rule. Add {lang="ts"} hint to
inline lintrk('track') so it's syntax-highlighted.
The schema's id field accepted an empty array because minLength(1)
constrained array elements but not the array itself. An empty array
produced a silent no-op install: window._linkedin_partner_id was
written as undefined, _linkedin_data_partner_ids stayed empty, and
the loaded script had no Partner ID to report against. Wrap the
array branch with pipe(..., minLength(1)) so dev-mode schema
validation rejects it at config time.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 3, 2026

@zizzfizzix is attempting to deploy a commit to the Nuxt Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@741

commit: 0b07b39

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds LinkedIn Insight Tag support: new docs page; logo assets; Valibot schema, TypeScript types, and runtime composable useScriptLinkedInInsight; registry entry with proxy and Partytown configuration; script metadata; playground pages and fixtures for bundled/CDN modes; unit/type updates; and a comprehensive E2E test suite covering script loading, SPA tracking, conversion/event deduplication, and enhanced matching behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add LinkedIn Insight Tag' clearly and concisely describes the main change—adding LinkedIn Insight Tag support as a new registry entry with corresponding composable, documentation, and test coverage.
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.
Description check ✅ Passed The PR description clearly articulates the feature being added (LinkedIn Insight Tag as a registry entry) and provides comprehensive implementation details, test coverage, and design rationale.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/script/src/runtime/registry/linkedin-insight.ts`:
- Around line 82-84: The stubbed lintrk currently enqueues only [a, b] which
drops any extra arguments permitted by the LintrkFns signature; update the
lintrk stub to capture and push the full arguments array (e.g., collect all
arguments into an array and push that array into (lintrk as any).q) so it
preserves arbitrary arity, and ensure the function still matches the
LinkedInInsightApi['lintrk']/LintrkFns types.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edd634e1-7d53-41a2-9c7d-fafbb39edb89

📥 Commits

Reviewing files that changed from the base of the PR and between 0bdd936 and ab3ba6b.

📒 Files selected for processing (13)
  • docs/content/scripts/linkedin-insight.md
  • packages/script/src/registry-logos.ts
  • packages/script/src/registry-types.json
  • packages/script/src/registry.ts
  • packages/script/src/runtime/registry/linkedin-insight.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/types.ts
  • packages/script/src/script-meta.ts
  • test/fixtures/first-party/nuxt.config.ts
  • test/fixtures/first-party/pages/linkedin.vue
  • test/types/types.test-d.ts
  • test/unit/first-party.test.ts
  • test/unit/proxy-configs.test.ts

Comment thread packages/script/src/runtime/registry/linkedin-insight.ts Outdated
zizzfizzix and others added 6 commits May 3, 2026 08:46
CodeRabbit flagged: the queue stub was function (a, b) which dropped
extra arguments allowed by the LintrkFns escape-hatch overload
((cmd: string & {}, ...args: any[]) => void). Switch to rest
parameters so callers using the escape hatch don't lose arguments
before the real script binds.

Also adds JSDoc to the public function and types to satisfy the
project's docstring-coverage threshold.
Previous implementation registered useScriptEventPage inside clientInit,
which only runs once on first script load. The hook was tied to the
first page's component scope and torn down via onScopeDispose on unmount,
so subsequent SPA navigations had no hook to fire on.

Move useScriptEventPage out of clientInit so each component setup that
calls the composable registers its own hook (matches Matomo pattern).
When enableAutoSpaTracking is true, also set window._wait_for_lintrk to
suppress the script's built-in auto-page-view; otherwise the initial
page would fire two /collect beacons (one from the script's bootstrap,
one from useScriptEventPage's page:finish callback).

Read the merged enableAutoSpaTracking flag from optionsFn's options
parameter (which useRegistryScript already defu-merges with runtimeConfig)
so the flag works whether set at the call site or in registry config.

Drop dead _already_called_lintrk Window declaration.
Replaces the placeholder fixture page in test/fixtures/first-party/
(added in 4851936) with two dedicated fixtures that can actually verify
behavior. The first-party fixture's manual trigger prevented script
auto-loading, so the placeholder couldn't run real assertions.

The CDN fixture extends the bundled fixture as a Nuxt layer, sharing
app.vue, package.json, and pages/index.vue. The page files are still
overridden because per-call scriptOptions.bundle is the only place the
bundling decision actually flows into the AST transform's registryConfig
read; defaultScriptOptions.bundle alone doesn't propagate.

Suite runs 7 tests against each fixture (14 total), all in CI:
  - script tag points at the expected origin (bundled vs CDN)
  - writes partner ID globals before the script loads
  - initial page load fires exactly one canonical /collect (regression
    guard for the double-fire bug — verified to fail when the
    _wait_for_lintrk suppression is removed)
  - SPA navigation fires a track beacon (auto-page-view enabled)
  - SPA navigation fires no track beacon when enableAutoSpaTracking: false
  - lintrk('track', { conversion_id }) fires /collect with conversionId
  - setUserData populates localStorage[li_hem] and the next page load
    transmits it via /wa/ POST (decodes the gzip+base64 body and asserts
    SHA-256 of the email is in the payload)
- FIRST_PARTY.md and docs/content/docs/1.guides/2.first-party.md:
  add LinkedIn to the PRIVACY_FULL group and Ad Pixels list.
- docs/content/scripts/linkedin-insight.md: explain that setUserData
  transmits the hashed email via /wa/ on the next page load (not the
  same-page /collect), and that enableAutoSpaTracking suppresses the
  built-in auto-page-view so each route fires exactly one /collect.
Mirrors the redditPixel layout: a default.vue (manual install via useHead
script tag) and a nuxt-scripts.vue (using useScriptLinkedInInsight).
Registered in playground/pages/index.vue.
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
test/e2e/_linkedin-insight-suite.ts (1)

14-16: ⚡ Quick win

Consider a conditional skip for network-restricted CI environments.

The comment correctly notes that all tracking-beacon assertions silently become expected 0 to be greater than 0 when external egress is blocked. This produces misleading failures with no indication that network access, not logic, is the root cause. A simple guard using a custom env variable or vi.runIf would make these failures diagnosable at a glance.

💡 Example: env-variable-gated suite
+const NETWORK_AVAILABLE = process.env.LINKEDIN_E2E_NETWORK !== '0'
+
 export function defineLinkedInInsightSuite(opts: SuiteOptions) {
-  it('script tag points at the expected origin', async () => {
+  const nit = NETWORK_AVAILABLE ? it : it.skip
+  nit('script tag points at the expected origin', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/_linkedin-insight-suite.ts` around lines 14 - 16, Add a conditional
skip around the LinkedIn insight e2e suite so it doesn't fail in
network-restricted CI: detect a gating env var (e.g.,
process.env.RUN_NETWORK_TESTS or process.env.SKIP_EXTERNAL_EGRESS) at the top of
the test file and wrap the suite with vi.runIf(process.env.RUN_NETWORK_TESTS ===
'1')(... ) or early-return/describe.skip when the var is not set; update the
file's top-level test suite (the describe/it block that contains the
"tracking-beacon" assertions) to use vi.runIf or the env check so the assertions
are skipped in CI without egress.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/e2e/_linkedin-insight-suite.ts`:
- Around line 14-16: Add a conditional skip around the LinkedIn insight e2e
suite so it doesn't fail in network-restricted CI: detect a gating env var
(e.g., process.env.RUN_NETWORK_TESTS or process.env.SKIP_EXTERNAL_EGRESS) at the
top of the test file and wrap the suite with
vi.runIf(process.env.RUN_NETWORK_TESTS === '1')(... ) or
early-return/describe.skip when the var is not set; update the file's top-level
test suite (the describe/it block that contains the "tracking-beacon"
assertions) to use vi.runIf or the env check so the assertions are skipped in CI
without egress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0eb7833-0e1f-41e9-a5c0-f3e533855fd2

📥 Commits

Reviewing files that changed from the base of the PR and between ef95bac and fc7e3fa.

📒 Files selected for processing (25)
  • FIRST_PARTY.md
  • docs/content/docs/1.guides/2.first-party.md
  • docs/content/scripts/linkedin-insight.md
  • package.json
  • packages/script/src/registry-types.json
  • packages/script/src/runtime/registry/linkedin-insight.ts
  • playground/pages/index.vue
  • playground/pages/third-parties/linkedin-insight/default.vue
  • playground/pages/third-parties/linkedin-insight/nuxt-scripts.vue
  • test/e2e/_linkedin-insight-suite.ts
  • test/e2e/linkedin-insight-cdn.test.ts
  • test/e2e/linkedin-insight.test.ts
  • test/fixtures/linkedin-insight-cdn/nuxt.config.ts
  • test/fixtures/linkedin-insight-cdn/pages/linkedin-no-spa.vue
  • test/fixtures/linkedin-insight-cdn/pages/linkedin-spa.vue
  • test/fixtures/linkedin-insight-cdn/pages/linkedin.vue
  • test/fixtures/linkedin-insight-cdn/tsconfig.json
  • test/fixtures/linkedin-insight/app.vue
  • test/fixtures/linkedin-insight/nuxt.config.ts
  • test/fixtures/linkedin-insight/package.json
  • test/fixtures/linkedin-insight/pages/index.vue
  • test/fixtures/linkedin-insight/pages/linkedin-no-spa.vue
  • test/fixtures/linkedin-insight/pages/linkedin-spa.vue
  • test/fixtures/linkedin-insight/pages/linkedin.vue
  • test/fixtures/linkedin-insight/tsconfig.json
✅ Files skipped from review due to trivial changes (9)
  • test/fixtures/linkedin-insight/tsconfig.json
  • test/fixtures/linkedin-insight-cdn/tsconfig.json
  • test/fixtures/linkedin-insight/app.vue
  • test/fixtures/linkedin-insight-cdn/nuxt.config.ts
  • test/fixtures/linkedin-insight-cdn/pages/linkedin-spa.vue
  • test/fixtures/linkedin-insight/package.json
  • package.json
  • docs/content/docs/1.guides/2.first-party.md
  • FIRST_PARTY.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/script/src/registry-types.json

…offline

Tests fall into two groups: "wiring" assertions (script tag in DOM, partner
ID globals set by clientInit) work offline because they observe state that
exists before the LinkedIn script executes. "Behavior" assertions need the
script to actually run and fire tracking, which requires reaching
snap.licdn.com (CDN mode) and px.ads.linkedin.com (cookie-test bootstrap).

Probe both endpoints once in beforeAll; behavior tests call ctx.skip() when
egress is unavailable. Wiring tests rewritten to wait on the actual signal
they assert (script tag attached, _linkedin_partner_id defined) instead of
the #status:has-text("loaded") sentinel that requires a successful fetch.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/e2e/_linkedin-insight-suite.ts`:
- Line 142: Replace the fixed sleeps (calls to page.waitForTimeout(...)) with a
deterministic polling loop that checks the test's captured request state (e.g.,
the array/object you use to record outgoing beacons/requests such as
capturedRequests or requests) until the expected condition (specific
beacon/request count or a particular request URL/payload) is met or a reasonable
timeout is reached; locate each page.waitForTimeout invocation in
test/e2e/_linkedin-insight-suite.ts and implement a short-poll loop that queries
the captured request store and breaks when the assertion condition is satisfied,
failing the test if the timeout elapses.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afe4d7a3-a721-4862-ba6f-b6bff860499f

📥 Commits

Reviewing files that changed from the base of the PR and between 41e3d4b and 067cd25.

📒 Files selected for processing (1)
  • test/e2e/_linkedin-insight-suite.ts

Comment thread test/e2e/_linkedin-insight-suite.ts Outdated
zizzfizzix added 2 commits May 3, 2026 23:09
Hard-coded waitForTimeout(2000/3000) calls can intermittently miss delayed
beacons and slow healthy runs. Replace with two helpers:

- waitFor(predicate): poll until expected request appears (positive
  assertions). Used in the SPA, conversion, and setUserData /wa/ tests.
- waitForStable(count): poll until the count stops changing for a settle
  window. Used by the double-fire regression guard (must let a second
  beacon happen if it's going to) and the negative SPA-tracking test
  (must let a late beacon happen if it's going to).

Healthy runs now finish faster (~26s for 14 tests vs ~30s).
@zizzfizzix
Copy link
Copy Markdown
Contributor Author

Done here, @harlan-zw let me know if any more changes are required

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