Skip to content

feat: Ahrefs Web Analytics#751

Merged
harlan-zw merged 5 commits intomainfrom
feat/ahrefs-analytics
May 8, 2026
Merged

feat: Ahrefs Web Analytics#751
harlan-zw merged 5 commits intomainfrom
feat/ahrefs-analytics

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented May 7, 2026

🔗 Linked issue

Related to #177

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Adds Ahrefs Web Analytics to the registry as useScriptAhrefsAnalytics. Bundled and proxied via analytics.ahrefs.com with PRIVACY_IP_ONLY; the single domain covers both /analytics.js and the /api/event beacon. SPA tracking works out of the box because analytics.js natively patches history.pushState and listens for popstate (the originally-planned enableAutoSpaTracking flag was dropped after confirming this). Schema rejects empty key so misconfig fails dev validation.

🧪 Usage

<script setup lang="ts">
const { proxy } = useScriptAhrefsAnalytics({
  key: 'your-project-key',
})

function trackSignup() {
  proxy.AhrefsAnalytics.sendEvent('signup', {
    props: { plan: 'pro' },
  })
}
</script>

Add ahrefsAnalytics: { key: 'your-project-key' } under scripts.registry in nuxt.config.ts to register the script for first-party proxy + bundling.

Adds the `useScriptAhrefsAnalytics` composable for Ahrefs's privacy-first,
cookie-less web analytics. Bundles `analytics.js` and proxies the beacon
endpoint through `/_scripts/p/` (`PRIVACY_IP_ONLY`). SPA navigation is
auto-tracked by the script's native `history.pushState` patch, so no
`enableAutoSpaTracking` flag is required.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment May 8, 2026 4:05am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 7, 2026

Open in StackBlitz

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

commit: 8e4b9fb

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request integrates Ahrefs Web Analytics support into a Nuxt Scripts framework. The changes include a new ahrefsAnalytics registry entry with type definitions, schema validation, and runtime composable implementation (useScriptAhrefsAnalytics). Documentation is updated across guides and a new dedicated page, along with playground examples demonstrating bundled and CDN-based loading modes. Test fixtures and E2E test suites are added to validate both scenarios, including event tracking and SPA navigation behavior. Build scripts are updated to prepare test fixtures. The integration uses PRIVACY_IP_ONLY tier privacy settings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% 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: Ahrefs Web Analytics' clearly and concisely summarizes the main change—adding support for Ahrefs Web Analytics as a new feature to the scripts registry.
Description check ✅ Passed The description clearly explains the feature being added, including technical details about bundling, proxying, privacy configuration, schema validation, and provides usage examples with code snippets directly related to the changeset.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ahrefs-analytics

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/1.guides/2.first-party.md`:
- Line 66: The "IP only" table row incorrectly lists Google AdSense; per
FIRST_PARTY.md AdSense uses the same proxy config as Google Analytics and
belongs to PRIVACY_HEATMAP. Remove Google AdSense from the "IP only" row and add
it to the "Heatmap-safe" row alongside Google Analytics (ensure the term "Google
AdSense" matches existing casing and that you reference
PRIVACY_HEATMAP/Heatmap-safe in the docs to keep consistency with
FIRST_PARTY.md).
🪄 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: 721d4162-fd89-4b88-8439-3542c861e9fe

📥 Commits

Reviewing files that changed from the base of the PR and between 6ddc9f6 and adb0118.

📒 Files selected for processing (29)
  • FIRST_PARTY.md
  • docs/content/docs/1.guides/2.first-party.md
  • docs/content/scripts/ahrefs-analytics.md
  • package.json
  • packages/script/src/registry-logos.ts
  • packages/script/src/registry-types.json
  • packages/script/src/registry.ts
  • packages/script/src/runtime/registry/ahrefs-analytics.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/types.ts
  • packages/script/src/script-meta.ts
  • playground/pages/index.vue
  • playground/pages/third-parties/ahrefs-analytics/default.vue
  • playground/pages/third-parties/ahrefs-analytics/nuxt-scripts.vue
  • test/e2e/_ahrefs-analytics-suite.ts
  • test/e2e/ahrefs-analytics-cdn.test.ts
  • test/e2e/ahrefs-analytics.test.ts
  • test/fixtures/ahrefs-analytics-cdn/nuxt.config.ts
  • test/fixtures/ahrefs-analytics-cdn/pages/ahrefs.vue
  • test/fixtures/ahrefs-analytics-cdn/tsconfig.json
  • test/fixtures/ahrefs-analytics/app.vue
  • test/fixtures/ahrefs-analytics/nuxt.config.ts
  • test/fixtures/ahrefs-analytics/package.json
  • test/fixtures/ahrefs-analytics/pages/ahrefs.vue
  • test/fixtures/ahrefs-analytics/pages/index.vue
  • test/fixtures/ahrefs-analytics/tsconfig.json
  • test/types/types.test-d.ts
  • test/unit/first-party.test.ts
  • test/unit/proxy-configs.test.ts

| **Full** | IP, user agent, language, screen, timezone, hardware fingerprints | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag |
| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Google AdSense is listed under the wrong privacy tier.

The "IP only" row here includes Google AdSense, but according to FIRST_PARTY.md, AdSense shares Google Analytics's proxy config and uses PRIVACY_HEATMAP (IP + language + hardware). It should either be removed from this row or moved to Heatmap-safe alongside Google Analytics.

📝 Suggested fix
-| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
+| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy |

And update the Heatmap-safe row:

-| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
+| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Google AdSense, Microsoft Clarity, Hotjar |
🤖 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 `@docs/content/docs/1.guides/2.first-party.md` at line 66, The "IP only" table
row incorrectly lists Google AdSense; per FIRST_PARTY.md AdSense uses the same
proxy config as Google Analytics and belongs to PRIVACY_HEATMAP. Remove Google
AdSense from the "IP only" row and add it to the "Heatmap-safe" row alongside
Google Analytics (ensure the term "Google AdSense" matches existing casing and
that you reference PRIVACY_HEATMAP/Heatmap-safe in the docs to keep consistency
with FIRST_PARTY.md).

Probing analytics.ahrefs.com proved the script is reachable on CI but
beacons silently drop because test-ahrefs-key isn't registered to the
test origin. Match the Usercentrics pattern: only run behavioural
assertions when AHREFS_TEST_KEY is set; wiring assertions still run
unconditionally.
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 (2)
test/e2e/_ahrefs-analytics-suite.ts (2)

109-109: 💤 Low value

waitForURL timeout of 5 000 ms is significantly shorter than the surrounding assertions

Every other waitFor / waitForSelector in this suite uses 10–15 s timeouts, but page.waitForURL here uses 5 000 ms. A slower CI runner or a brief network hiccup during SPA navigation could hit this limit while the overall 60 s test budget still has plenty of time left.

♻️ Proposed change
-      await page.waitForURL('**/', { timeout: 5000 })
+      await page.waitForURL('**/', { timeout: 15000 })
🤖 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 `@test/e2e/_ahrefs-analytics-suite.ts` at line 109, The page.waitForURL call
uses a 5000ms timeout which is much shorter than surrounding waits and can flake
on slow CI; update the call to use the same longer timeout as nearby waits
(e.g., 10000–15000 ms) so page.waitForURL('**/', { timeout: 10000 }) (or 15000)
matches the suite's timing expectations and reduces intermittent failures.

26-37: ⚡ Quick win

Consider page.waitForResponse / Playwright's built-in request interception instead of the custom busy-poll helper

waitFor busy-polls at 50 ms intervals, burning CPU for up to 10 seconds on every beacon assertion. Playwright already exposes page.waitForResponse(predicate) which is event-driven and integrates cleanly with the existing request list. This also removes the hand-rolled timeout logic.

♻️ Example using `page.waitForResponse`
-async function waitFor(
-  predicate: () => boolean,
-  { timeoutMs = 10000, intervalMs = 50, message = 'condition' }: { timeoutMs?: number, intervalMs?: number, message?: string } = {},
-) {
-  const deadline = Date.now() + timeoutMs
-  while (Date.now() < deadline) {
-    if (predicate())
-      return
-    await new Promise(r => setTimeout(r, intervalMs))
-  }
-  throw new Error(`Timed out after ${timeoutMs}ms waiting for ${message}`)
-}

Then replace e.g.:

// Before
await waitFor(() => matches().length > 0, { message: '/api/event beacon' })
expect(matches().length).toBeGreaterThan(0)

// After
await page.waitForResponse(
  res => res.request().method() === 'POST'
    && (res.url().includes('analytics.ahrefs.com/api/event') || res.url().includes('/analytics.ahrefs.com/api/event')),
  { timeout: 10000 },
)
🤖 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 `@test/e2e/_ahrefs-analytics-suite.ts` around lines 26 - 37, The custom
busy-poll helper waitFor (async function waitFor(...)) should be replaced by
Playwright's event-driven page.waitForResponse to avoid CPU spinning and
duplicate timeout logic; locate usages of waitFor and replace them with
page.waitForResponse calls that match the beacon POST requests (check
res.request().method() and res.url().includes(...) predicates) and pass an
appropriate timeout option (e.g., 10000ms), then remove the waitFor helper
function if no other references remain.
🤖 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.

Nitpick comments:
In `@test/e2e/_ahrefs-analytics-suite.ts`:
- Line 109: The page.waitForURL call uses a 5000ms timeout which is much shorter
than surrounding waits and can flake on slow CI; update the call to use the same
longer timeout as nearby waits (e.g., 10000–15000 ms) so page.waitForURL('**/',
{ timeout: 10000 }) (or 15000) matches the suite's timing expectations and
reduces intermittent failures.
- Around line 26-37: The custom busy-poll helper waitFor (async function
waitFor(...)) should be replaced by Playwright's event-driven
page.waitForResponse to avoid CPU spinning and duplicate timeout logic; locate
usages of waitFor and replace them with page.waitForResponse calls that match
the beacon POST requests (check res.request().method() and
res.url().includes(...) predicates) and pass an appropriate timeout option
(e.g., 10000ms), then remove the waitFor helper function if no other references
remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52fd84aa-899d-4d99-840f-ca69dffc84de

📥 Commits

Reviewing files that changed from the base of the PR and between adb0118 and 2c16aec.

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

Replaces the AHREFS_TEST_KEY env-var skip with a Playwright network stub.
The real analytics.js bails out on localhost before posting /api/event, so
we also stub the script itself with a minimal pageview-firing replacement
that mirrors the integration shape (initial POST + history.pushState patch).
Both behavioural tests now run unconditionally on CI; assertions verify
method, origin, path, and non-empty payload for every captured beacon.
harlan-zw added 2 commits May 8, 2026 13:55
Bundled mode silently dropped every Ahrefs beacon: analytics.js computes
its endpoint as `new URL(currentScript.src).origin + "/api/event"`, and
when bundled the script src origin is the local Nuxt origin, so beacons
landed on a 404. Add a `replace-new-url-origin` sdkPatch (same family as
Fathom's neutralize-domain-check and Rybbit's replace-src-split) that
rewrites the derivation to `(self.location.origin + "<proxyPath>")`,
wire it into the ahrefsAnalytics proxy def, cover the AST visitor in the
bundle-sdk-patches integration suite, and harden the e2e suite to fetch
the unstubbed bundle and assert the rewritten endpoints — the prior
stub encoded the bug as expected behaviour.

Verified live against analytics.ahrefs.com: visitors are counted.
Match the structure used by fathom-analytics / cloudflare-web-analytics /
plausible-analytics: 2-space `links:` indentation, defaults note,
proxy/onLoaded code-group for the basic API, ::script-types:: at the
end, and a "Loading in app.vue" example matching the rest of the
analytics docs. Keep the SPA-tracking note since native pushState
patching is a real differentiator worth surfacing.
@harlan-zw harlan-zw merged commit ceb4ba0 into main May 8, 2026
18 of 19 checks passed
@harlan-zw harlan-zw deleted the feat/ahrefs-analytics branch May 8, 2026 04:08
harlan-zw added a commit that referenced this pull request May 8, 2026
The composable injected `<link rel=stylesheet href=https://assets.calendly.com/assets/external/widget.css>`
which leaked the visitor IP to the vendor on every page render — and the
`url(/assets/external/close-icon.svg)` reference inside that stylesheet
leaked again on every popup-close. This bypassed the bundle/proxy
posture the registry advertises (`proxy.privacy: PRIVACY_IP_ONLY`).

Inlines the 2.4 KB stylesheet via `useHead({ style })`, with the
close-icon SVG embedded as a data URI. No more requests to
`assets.calendly.com` at any point in the widget lifecycle.

Adds `<ScriptCalendlyInlineWidget>` for the inline embed shape (popup
and badge stay composable-only, since they have no host element).
Mirrors `<ScriptYouTubePlayer>` — visibility trigger by default,
`above-the-fold` preconnect, slots for loading/awaiting/error.

Hardens the e2e suite to match the Ahrefs bar: asserts no
`assets.calendly.com` stylesheet link is present, that the inline
`<style>` carries the data-URI close icon, and that
`initInlineWidget` mounts a real iframe in the requested
parentElement. Adds a Node-fetch contract test that asserts the
bundled artefact still exports the widget API and contains no
`assets.calendly.com` references.

Docs aligned with the post-#751 layout (frontmatter component link,
`script-types` at end, code-group for proxy/onLoaded, dedicated
`<ScriptCalendlyInlineWidget>` section).
@harlan-zw harlan-zw mentioned this pull request May 8, 2026
6 tasks
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