feat: add LinkedIn Insight Tag#741
Conversation
…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.
|
@zizzfizzix is attempting to deploy a commit to the Nuxt Team on Vercel. A member of the Team first needs to authorize it. |
commit: |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds LinkedIn Insight Tag support: new docs page; logo assets; Valibot schema, TypeScript types, and runtime composable Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (13)
docs/content/scripts/linkedin-insight.mdpackages/script/src/registry-logos.tspackages/script/src/registry-types.jsonpackages/script/src/registry.tspackages/script/src/runtime/registry/linkedin-insight.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/types.tspackages/script/src/script-meta.tstest/fixtures/first-party/nuxt.config.tstest/fixtures/first-party/pages/linkedin.vuetest/types/types.test-d.tstest/unit/first-party.test.tstest/unit/proxy-configs.test.ts
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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/e2e/_linkedin-insight-suite.ts (1)
14-16: ⚡ Quick winConsider 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 0when 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 orvi.runIfwould 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
📒 Files selected for processing (25)
FIRST_PARTY.mddocs/content/docs/1.guides/2.first-party.mddocs/content/scripts/linkedin-insight.mdpackage.jsonpackages/script/src/registry-types.jsonpackages/script/src/runtime/registry/linkedin-insight.tsplayground/pages/index.vueplayground/pages/third-parties/linkedin-insight/default.vueplayground/pages/third-parties/linkedin-insight/nuxt-scripts.vuetest/e2e/_linkedin-insight-suite.tstest/e2e/linkedin-insight-cdn.test.tstest/e2e/linkedin-insight.test.tstest/fixtures/linkedin-insight-cdn/nuxt.config.tstest/fixtures/linkedin-insight-cdn/pages/linkedin-no-spa.vuetest/fixtures/linkedin-insight-cdn/pages/linkedin-spa.vuetest/fixtures/linkedin-insight-cdn/pages/linkedin.vuetest/fixtures/linkedin-insight-cdn/tsconfig.jsontest/fixtures/linkedin-insight/app.vuetest/fixtures/linkedin-insight/nuxt.config.tstest/fixtures/linkedin-insight/package.jsontest/fixtures/linkedin-insight/pages/index.vuetest/fixtures/linkedin-insight/pages/linkedin-no-spa.vuetest/fixtures/linkedin-insight/pages/linkedin-spa.vuetest/fixtures/linkedin-insight/pages/linkedin.vuetest/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.
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
test/e2e/_linkedin-insight-suite.ts
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).
|
Done here, @harlan-zw let me know if any more changes are required |
Summary
Adds the LinkedIn Insight Tag as a first-class registry entry under the
adcategory, alongside Bing UET / Meta / TikTok / Reddit / X / Snapchat pixels.useScriptLinkedInInsight()exposing a typedlintrkproxy with overloads fortrackandsetUserDataclientInitordering matches the LinkedIn dedup doc requirement to setwindow._linkedin_event_idbefore the base code runs_linkedin_data_partner_ids)eventIdfor page-load Conversions API deduplicationenableAutoSpaTrackingopt-in to virtual page-views viauseScriptEventPage(mirrors the Matomo pattern); when enabled,window._wait_for_lintrk = truesuppresses the script's built-in auto-page-view so each route fires exactly onelintrk('track')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 pixelDesign notes
defaultConsentschema field. LinkedIn does not expose alintrk('consent', …)command (verified against the publishedinsight.min.jssource — onlytrackandsetUserDatacases exist in the dispatch switch). This matches X Pixel, Reddit Pixel, and Snapchat Pixel, which also have nodefaultConsent. Consent gating is achieved today viauseScript's existingtriggeroption. 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.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 ownlintrk('track')(no synthetic image-pixel calls).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) — passestest/e2e/linkedin-insight.test.ts(bundled) +test/e2e/linkedin-insight-cdn.test.ts(CDN), each running the same 7-test suite fromtest/e2e/_linkedin-insight-suite.ts:/_scripts/assets/(bundled) orsnap.licdn.com(CDN)clientInitbefore the script loads/collectbeacon whenenableAutoSpaTracking: true/collect(catches double-fire if_wait_for_lintrksuppression breaks)/collectwhenenableAutoSpaTracking: falselintrk('track', { conversion_id })includesconversionIdin the URLsetUserDatapopulateslocalStorage[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/linkedin-insight-cdnextendstest/fixtures/linkedin-insightand only overridesbundle: falsesnap.licdn.com+px.ads.linkedin.comonce inbeforeAll; behavior tests skip automatically when egress is unavailable, while the two wiring tests (DOM + globals) run unconditionallyproxy-configs.test.tsandfirst-party.test.tsassertions extended withlinkedinInsightexpectTypeOf<Registry['linkedinInsight']>().not.toBeAny()pnpm generate:typesreproduces the committedregistry-types.json/collectrequests reach LinkedIn under both direct and first-party-proxy modes