Skip to content

feat: score user sessions#55

Merged
vitonsky merged 11 commits intomasterfrom
52-score-user-sessions
Mar 8, 2026
Merged

feat: score user sessions#55
vitonsky merged 11 commits intomasterfrom
52-score-user-sessions

Conversation

@vitonsky
Copy link
Copy Markdown
Owner

@vitonsky vitonsky commented Mar 8, 2026

Closes #52

Summary by CodeRabbit

  • New Features

    • Session scoring: emits session quality and bot-detection metrics.
    • Enhanced outbound link tracking with configurable event name, optional link-text capture, and filtering.
    • Link-click capture plugin for configurable link event reporting.
    • Cookie-backed storage option for persistent data.
  • Refactor

    • Consolidated plugins under a single plugins surface.
    • Introduced a browser UID storage abstraction for identifier handling.
  • Tests

    • Added unit tests for outbound tracking and session scoring.
  • Chores

    • Spellchecker updated to skip "cpu".

@vitonsky vitonsky linked an issue Mar 8, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 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

Restructures tracking code into a plugins surface, adds session-scoring and link-click plugins and tests, extracts cookie-based Storage and a UID storage abstraction, re-exports plugins from src/plugins, updates test jsdom URL, and adds an ESLint spellcheck word.

Changes

Cohort / File(s) Summary
Root & exports
\.eslintrc, src/index.ts, src/plugins/index.ts
Added "cpu" to spellcheck; replaced direct top-level plugin exports with export * from './plugins' and added plugin re-exports.
Outbound tracking (moved/removed)
src/enableAutoOutboundTracking.ts, src/plugins/enableAutoOutboundTracking.ts, src/plugins/enableAutoOutboundTracking.test.ts
Removed top-level enableAutoOutboundTracking; added plugin-scoped implementation delegating to enableLinkClicksCapture with same-origin filtering and new tests verifying payloads and text capture.
Link click capture plugin
src/plugins/enableLinkClicksCapture.ts
New plugin exposing LinkClickPluginConfig and enableLinkClicksCapture, attaches capture-phase click listener, finds anchors via composedPath, supports optional text capture and filtering, emits Plausible events, and returns cleanup.
Pageviews plugin path
src/plugins/enableAutoPageviews.ts
Adjusted import paths to parent directory (no logic change); re-exported via plugins index.
Session scoring plugin
src/plugins/enableSessionScoring.ts, src/plugins/enableSessionScoring.test.ts, src/Plausible.server-environment.test.ts
New getBotSignals and enableSessionScoring emit a "Session scored" event with bot signals, device/timezone/session metrics, configurable storage (including CookieStorage), and next-frame scheduling; tests added and plugin enabled in server-environment test.
Storage refactor & UID
src/transformers/CookieStorage.ts, src/transformers/userId.ts, src/transformers/userId.test.ts
Extracted CookieStorage as a Storage implementation; introduced UIDStorage interface and BrowserUIDStorage class; refactored userId to use the storage abstraction and updated imports.
Plugin re-exports & other plugins
src/plugins/...
Added and re-exported plugin modules (enableLinkClicksCapture, enableAutoOutboundTracking, enableSessionScoring, plus existing pageviews) from src/plugins/index.ts.
Test config
vitest.config.ts
Configured environmentOptions.jsdom.url = 'https://example.org' for consistent jsdom URL in tests.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/App
    participant Session as enableSessionScoring
    participant Storage as Storage (localStorage / CookieStorage)
    participant BotDetect as getBotSignals
    participant Plausible as Plausible SDK
    participant Network as Network (POST /api/event)

    Browser->>Session: enableSessionScoring(plausible, config)
    alt running in browser
        Session->>Storage: read/initialize firstVisit
        Storage-->>Session: firstVisit timestamp
        Session->>Session: requestAnimationFrame (next frame)
        Session->>BotDetect: getBotSignals()
        BotDetect->>BotDetect: evaluate navigator, UA, plugins, languages, permissions, hw
        BotDetect-->>Session: {isBot, score, signals}
        Session->>Plausible: event("Session scored", {botScore, botSignals, sessionAge, ...})
        Plausible->>Network: POST /api/event (keepalive: true)
        Network-->>Plausible: 200 OK
    else server-side
        Session-->>Browser: noop
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 I hopped through plugins, tidy and bright,

Scored sessions at dawn and chased links by night,
Cookies tucked safely, a UID in store,
Tests tapped their paws and shouted encore,
A merry little hop — tracking and more ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes include restructuring plugins directory, refactoring enableAutoOutboundTracking into plugins folder, refactoring userId storage with CookieStorage, and adding bot detection logic—all scope creep beyond core session scoring feature. Separate plugin directory restructuring and CookieStorage refactoring into dedicated PRs to keep session scoring focused and easier to review.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No PR description provided beyond the auto-generated link to issue #52; lacks detail about implementation approach, design decisions, or testing strategy. Add a description explaining the session scoring implementation, bot detection logic, storage strategy, and any design trade-offs made.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: score user sessions' directly matches the primary objective from issue #52, clearly summarizing the main addition of enableSessionScoring functionality.
Linked Issues check ✅ Passed The PR implements enableSessionScoring with bot signal detection, device features, and region signals; provides extensibility via storage config; meets all stated requirements from issue #52.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 52-score-user-sessions

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: 8

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

Inline comments:
In `@src/plugins/enableAutoOutboundTracking.test.ts`:
- Around line 25-80: The tests use internal fragment links but expect "Outbound
Link: Click" events; update the fixtures and assertions so positive cases use
real external URLs (e.g., replace href="#foo" / "#baz" with
"https://external.example/..." in document.body and adapt expected p.url
accordingly) and keep captureText behavior for link text (span click). Also add
a negative test case that clicks a same-page/hash link (e.g., href="#bar") and
asserts mockFetch was not called (or call count unchanged) to ensure
enableAutoOutboundTracking does not report internal fragment links; reference
the test names ("Capture click for simple link", "Capture click for link text")
and the enableAutoOutboundTracking import to locate where to change fixtures and
expectations.

In `@src/plugins/enableAutoOutboundTracking.ts`:
- Around line 31-42: Before sending the outbound event, parse the clicked href
with new URL(linkElement.href, document.baseURI) and skip sending if the link is
internal: return early when urlObj.origin === location.origin (same-origin) and
also return when the link only changes the hash on the current page
(urlObj.pathname === location.pathname && urlObj.search === location.search but
urlObj.hash !== location.hash). Keep the existing filter(url, text) check
afterward and only call plausible.sendEvent(eventName, { props: { url, text:
captureText ? text : undefined } }) for external/outbound links.
- Around line 26-32: The find() callback needs an explicit type predicate so the
returned linkElement is recognized as an HTMLAnchorElement and textContent must
be null-guarded before calling trim(); update the call to
event.composedPath().find(...) to use a predicate like function isAnchor(node):
node is HTMLAnchorElement { return node instanceof HTMLAnchorElement } (or
inline equivalent) so linkElement is typed as HTMLAnchorElement, then get text
with something like (linkElement.textContent ?? '').trim() (leave url =
linkElement.href as-is).

In `@src/plugins/enableSessionScoring.test.ts`:
- Around line 24-38: The test currently snapshots the full machine-dependent
scoring payload (via mockFetch.mock.calls) which makes it flaky; instead update
the test for enableSessionScoring to avoid a full snapshot: keep triggering the
scoring (create Plausible, call enableSessionScoring,
vi.advanceTimersToNextFrame), then assert that mockFetch was called (e.g.,
mockFetch.mock.calls.length > 0) and inspect the request body of the first call
to assert only invariant fields (for example check that the payload contains
expected event_name, domain, and a numeric/boolean shape for scoring fields)
while explicitly excluding or asserting patterns for runtime-dependent fields
(timezone, hardwareConcurrency, screen dimensions) rather than snapshotting the
entire body; reference mockFetch.mock.calls and the enableSessionScoring
invocation to locate where to change assertions.

In `@src/plugins/enableSessionScoring.ts`:
- Around line 71-74: The enableSessionScoring export currently accepts only
plausible and a config with storage/firstVisitKey, but the API must support an
optional callback to add custom features to the scoring event; update the
enableSessionScoring signature to accept a new optional callback (e.g.,
customScoringHook or augmentScoringEvent) in the config object, thread that
callback through wherever the scoring event is built/sent so it can append/merge
extra properties, and update any related functions (the scoring event
creator/sender) to call this callback with the current session/context and merge
its return into the final event payload; also add/update tests to pass a mock
callback and assert its returned props are included in the emitted scoring
event.
- Around line 77-82: The storage selection and usages must be made fail-safe:
wrap the runtime resolution of storage (the local variable storage initialized
with localStorage/sessionStorage/CookieStorage and firstVisitKey) in a try/catch
so any exception accessing localStorage/sessionStorage falls back to new
CookieStorage(), and replace direct storage.getItem/setItem/removeItem calls
with a small safe wrapper (e.g., safeStorage.getItem/setItem/removeItem) that
catches and swallows quota/privacy errors (no-ops on failure) to make
reads/writes best-effort; update code paths that reference storage (the initial
resolver and subsequent usages around the firstVisitKey logic) to use this safe
wrapper.
- Around line 44-58: getBotSignals currently mutates score/signals inside
permission.query.then which runs after getBotSignals returns, so
permission_mismatch is dropped; change getBotSignals to be async and await the
permission.query() result (handle promise rejection with try/catch) before
computing/returning isBot and the payload, and apply the same async/await fix to
the duplicate block around lines 95-106; specifically locate getBotSignals, the
permission.query(...) usage, and the places that compute isBot/emit the payload
and ensure the permission.query path updates score and signals synchronously
before returning.
- Around line 113-115: The payload building code unconditionally calls
navigator.languages.join(',') which can throw in browsers without
navigator.languages; update the payload logic (where timeZone, language,
languages are set — the same block that calls getBotSignals()) to guard access
to navigator.languages before calling .join: check that navigator.languages is
an array (or truthy) and only then call join(',') otherwise provide a safe
fallback (e.g., empty string or the existing no_languages marker) so session
scoring doesn't throw in locked-down browsers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6f624f8-f408-4be0-88b6-2343b214ddf5

📥 Commits

Reviewing files that changed from the base of the PR and between 63648ff and c367fec.

⛔ Files ignored due to path filters (1)
  • src/plugins/__snapshots__/enableSessionScoring.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • .eslintrc
  • src/Plausible.server-environment.test.ts
  • src/enableAutoOutboundTracking.ts
  • src/index.ts
  • src/plugins/enableAutoOutboundTracking.test.ts
  • src/plugins/enableAutoOutboundTracking.ts
  • src/plugins/enableAutoPageviews.ts
  • src/plugins/enableSessionScoring.test.ts
  • src/plugins/enableSessionScoring.ts
  • src/plugins/index.ts
  • src/transformers/CookieStorage.ts
  • src/transformers/userId.test.ts
  • src/transformers/userId.ts
  • vitest.config.ts
💤 Files with no reviewable changes (1)
  • src/enableAutoOutboundTracking.ts

Comment on lines +44 to +58
// 6. Permissions API anomaly (common headless trick)
try {
const permission = (navigator as any).permissions;
if (permission) {
permission.query({ name: 'notifications' }).then((result: any) => {
if (Notification.permission === 'denied' && result.state === 'prompt') {
score += 1;
signals.push('permission_mismatch');
}
});
}
} catch {
score += 1;
signals.push('permissions_error');
}
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 | 🟠 Major

getBotSignals() drops the permissions signal.

permission.query(...).then(...) mutates score after getBotSignals() has already returned and after isBot is computed, so permission_mismatch is never reliably reflected in the emitted event. Make bot detection async and await it before building the payload.

Proposed fix
-export function getBotSignals(): BotDetectionResult {
+export async function getBotSignals(): Promise<BotDetectionResult> {
 	let score = 0;
 	const signals: string[] = [];
@@
 	try {
 		const permission = (navigator as any).permissions;
 		if (permission) {
-			permission.query({ name: 'notifications' }).then((result: any) => {
-				if (Notification.permission === 'denied' && result.state === 'prompt') {
-					score += 1;
-					signals.push('permission_mismatch');
-				}
-			});
+			const result = await permission.query({
+				name: 'notifications' as PermissionName,
+			});
+			if (Notification.permission === 'denied' && result.state === 'prompt') {
+				score += 1;
+				signals.push('permission_mismatch');
+			}
 		}
 	} catch {
 		score += 1;
@@
-	const botSignals = getBotSignals();
-	requestAnimationFrame(() => {
+	requestAnimationFrame(async () => {
+		const botSignals = await getBotSignals();
 		const firstVisit = getFirstVisit();

Also applies to: 95-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/enableSessionScoring.ts` around lines 44 - 58, getBotSignals
currently mutates score/signals inside permission.query.then which runs after
getBotSignals returns, so permission_mismatch is dropped; change getBotSignals
to be async and await the permission.query() result (handle promise rejection
with try/catch) before computing/returning isBot and the payload, and apply the
same async/await fix to the duplicate block around lines 95-106; specifically
locate getBotSignals, the permission.query(...) usage, and the places that
compute isBot/emit the payload and ensure the permission.query path updates
score and signals synchronously before returning.

Comment on lines +71 to +74
export const enableSessionScoring = (
plausible: Plausible,
config: { storage?: Storage; firstVisitKey?: string } = {},
) => {
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 | 🟠 Major

The custom scoring hook from the PR objective is still missing.

The linked requirement calls for an optional callback to append custom features to the scoring event, but this API only accepts storage and firstVisitKey. As written, consumers cannot add their own session-scoring props.

I can help sketch the callback shape and the matching tests if you want to add it in this PR.

Also applies to: 102-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/enableSessionScoring.ts` around lines 71 - 74, The
enableSessionScoring export currently accepts only plausible and a config with
storage/firstVisitKey, but the API must support an optional callback to add
custom features to the scoring event; update the enableSessionScoring signature
to accept a new optional callback (e.g., customScoringHook or
augmentScoringEvent) in the config object, thread that callback through wherever
the scoring event is built/sent so it can append/merge extra properties, and
update any related functions (the scoring event creator/sender) to call this
callback with the current session/context and merge its return into the final
event payload; also add/update tests to pass a mock callback and assert its
returned props are included in the emitted scoring event.

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.

♻️ Duplicate comments (1)
src/plugins/enableSessionScoring.test.ts (1)

36-48: ⚠️ Potential issue | 🟠 Major

Avoid exact assertions for runtime-dependent scoring fields.

This test is still flaky because it hard-codes machine-specific values like hardwareConcurrency, and it also dereferences mockFetch.mock.calls[0] before proving the request was sent. Assert the call first, then pin only invariant fields and use flexible matchers for environment-dependent ones.

Suggested change
-	const eventBody = JSON.parse(mockFetch.mock.calls[0][1].body);
+	expect(mockFetch).toHaveBeenCalled();
+	const eventBody = JSON.parse(String(mockFetch.mock.calls[0][1].body));
 	const eventName = eventBody.n;
 	const payload = JSON.parse(eventBody.p);
 	expect(payload).toMatchObject({
 		botScore: 1,
 		botSignals: 'no_plugins',
 		sessionAge: 0,
 		language: 'en-US',
 		languages: 'en-US,en',
-		screenSize: '0x0',
-		hardwareConcurrency: 16,
-		deviceMemory: 0,
-		devicePixelRatio: 1,
+		screenSize: expect.stringMatching(/^\d+x\d+$/),
+		hardwareConcurrency: expect.any(Number),
+		deviceMemory: expect.any(Number),
+		devicePixelRatio: expect.any(Number),
 	});
 	expect(eventName).toBe('Session scored');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/enableSessionScoring.test.ts` around lines 36 - 48, The test is
brittle because it dereferences mockFetch.mock.calls[0] before asserting a call
and hard-codes runtime-dependent values; first assert mockFetch was called
(e.g., expect(mockFetch).toHaveBeenCalled()), then parse the first call's body
(eventBody -> payload) and assert only invariant fields via toMatchObject
(botScore, botSignals, sessionAge, language) while using flexible Jest matchers
for environment-dependent fields (e.g., expect.any(Number) or
expect.stringMatching(...) for hardwareConcurrency, deviceMemory,
devicePixelRatio, screenSize, and languages) so the test no longer relies on
machine-specific exact values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/plugins/enableSessionScoring.test.ts`:
- Around line 36-48: The test is brittle because it dereferences
mockFetch.mock.calls[0] before asserting a call and hard-codes runtime-dependent
values; first assert mockFetch was called (e.g.,
expect(mockFetch).toHaveBeenCalled()), then parse the first call's body
(eventBody -> payload) and assert only invariant fields via toMatchObject
(botScore, botSignals, sessionAge, language) while using flexible Jest matchers
for environment-dependent fields (e.g., expect.any(Number) or
expect.stringMatching(...) for hardwareConcurrency, deviceMemory,
devicePixelRatio, screenSize, and languages) so the test no longer relies on
machine-specific exact values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32ef5699-db0e-4424-8f0b-d688a793bf87

📥 Commits

Reviewing files that changed from the base of the PR and between c367fec and b5e9fa5.

📒 Files selected for processing (1)
  • src/plugins/enableSessionScoring.test.ts

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: 2

🧹 Nitpick comments (1)
src/plugins/enableAutoOutboundTracking.test.ts (1)

21-26: Move plugin activation inside tests or beforeEach for proper isolation.

enableAutoOutboundTracking is called once at module scope, which means the click listener is never cleaned up between tests. If a test modifies state or if the listener accumulates side effects, subsequent tests may see unexpected behavior.

Proposed fix
 const plausible = new Plausible({
 	apiHost: 'https://plausible.io',
 	domain: 'example.org',
 });

-enableAutoOutboundTracking(plausible, { captureText: true });
+let cleanup: () => void;
+
+beforeEach(() => {
+	// ... existing beforeEach code ...
+	cleanup = enableAutoOutboundTracking(plausible, { captureText: true });
+});
+
+afterEach(() => {
+	cleanup();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/enableAutoOutboundTracking.test.ts` around lines 21 - 26, The
plugin activation is done at module scope (enableAutoOutboundTracking(plausible,
...)) which prevents cleanup between tests; move the call into each test or a
beforeEach block so each test gets a fresh setup, and ensure you remove the
listener in afterEach (either by calling the teardown/disposer returned by
enableAutoOutboundTracking if it returns one, or by calling the appropriate
Plausible instance cleanup method or removing the event listener manually) to
avoid cross-test leakage; update tests that reference the module-scope plausible
instance to create a new Plausible() per test or in beforeEach and store any
disposer in a variable so afterEach can call it to restore isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/enableAutoOutboundTracking.ts`:
- Around line 14-18: The filter(url, text) function incorrectly uses
url.startsWith(location.origin) which can misclassify external look-alike
domains; replace the prefix check with robust origin comparison by constructing
a URL object for the candidate (handling relative URLs by using location as base
when in browser) and compare its origin to location.origin (or use try/catch to
safely handle invalid URLs). Update the logic in filter to only return false
when new URL(url, location.href).origin === location.origin, and ensure the
check is guarded by the same typeof window !== 'undefined' condition and handles
URL construction errors gracefully.

In `@src/plugins/enableLinkClicksCapture.ts`:
- Around line 28-34: The code uses event.composedPath().find(...) which
TypeScript doesn't narrow to HTMLAnchorElement and then calls
linkElement.textContent.trim() which can be null; update the find call to use a
type predicate (e.g., (node): node is HTMLAnchorElement => node instanceof
HTMLAnchorElement) so linkElement is properly typed, and guard
linkElement.textContent before trimming (either return early if null or use a
safe fallback like an empty string) so calling .trim() cannot throw; refer to
linkElement, event.composedPath(), and textContent when making the changes.

---

Nitpick comments:
In `@src/plugins/enableAutoOutboundTracking.test.ts`:
- Around line 21-26: The plugin activation is done at module scope
(enableAutoOutboundTracking(plausible, ...)) which prevents cleanup between
tests; move the call into each test or a beforeEach block so each test gets a
fresh setup, and ensure you remove the listener in afterEach (either by calling
the teardown/disposer returned by enableAutoOutboundTracking if it returns one,
or by calling the appropriate Plausible instance cleanup method or removing the
event listener manually) to avoid cross-test leakage; update tests that
reference the module-scope plausible instance to create a new Plausible() per
test or in beforeEach and store any disposer in a variable so afterEach can call
it to restore isolation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b1c8c25-0cc4-45a8-8fcf-0c4b8456fae5

📥 Commits

Reviewing files that changed from the base of the PR and between b5e9fa5 and 817983c.

📒 Files selected for processing (4)
  • src/plugins/enableAutoOutboundTracking.test.ts
  • src/plugins/enableAutoOutboundTracking.ts
  • src/plugins/enableLinkClicksCapture.ts
  • src/plugins/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/plugins/index.ts

@vitonsky
Copy link
Copy Markdown
Owner Author

vitonsky commented Mar 8, 2026

@CodeRabbit generate patch to update readme according to changes from most recent release to current commit

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the README
cat README.md

echo "---GIT TAGS---"
git tag --sort=-version:refname | head -10

echo "---LATEST TAG---"
git describe --tags --abbrev=0

echo "---CHANGES SINCE TAG---"
LATEST_TAG=$(git describe --tags --abbrev=0)
git diff "$LATEST_TAG"..HEAD -- README.md

echo "---PLUGIN INDEX---"
cat src/plugins/index.ts 2>/dev/null || echo "not found"

echo "---ENABLE SESSION SCORING---"
cat src/plugins/enableSessionScoring.ts 2>/dev/null || echo "not found"

echo "---ENABLE LINK CLICKS---"
cat src/plugins/enableLinkClicksCapture.ts 2>/dev/null || echo "not found"

echo "---ENABLE OUTBOUND TRACKING---"
cat src/plugins/enableAutoOutboundTracking.ts 2>/dev/null || echo "not found"

echo "---INDEX TS---"
cat src/index.ts 2>/dev/null || echo "not found"

echo "---COOKIE STORAGE---"
cat src/transformers/CookieStorage.ts 2>/dev/null || echo "not found"

echo "---USER ID TS---"
cat src/transformers/userId.ts 2>/dev/null || echo "not found"

Length of output: 14762


@vitonsky: There are no git tags in the repository, so I've treated the current README.md as the baseline. Here is a patch that documents all new public API introduced in this PR (and since the repo's history): enableLinkClicksCapture, enableSessionScoring, getBotSignals, the updated enableAutoOutboundTracking config, and a fix for the CookieStorage() call that should be new CookieStorage().

--- a/README.md
+++ b/README.md
@@ -51,13 +51,55 @@ enableAutoPageviews(plausible);
 ## Automatically track outbound clicks
 
-To track outbound clicks automatically, call `enableAutoOutboundTracking`:
+To track outbound clicks automatically (same-origin links are skipped automatically), call `enableAutoOutboundTracking`:
 
 ```ts
 import { Plausible, enableAutoOutboundTracking } from 'plausible-client';
 
 const plausible = new Plausible({
   apiHost: 'https://plausible.io',
   domain: 'example.org',
 });
 
-// Function returns cleanup callback and starts track outbound clicks
+// Function returns cleanup callback and starts tracking outbound clicks
 enableAutoOutboundTracking(plausible);
+
+// Optionally capture link text or apply a custom filter
+enableAutoOutboundTracking(plausible, {
+  captureText: true,
+  filter: (url, text) => !url.includes('internal'),
+});

+## Track any link clicks
+
+For fine-grained control over which links to track, use enableLinkClicksCapture directly:
+
+```ts
+import { Plausible, enableLinkClicksCapture } from 'plausible-client';
+
+const plausible = new Plausible({

+// Function returns cleanup callback
+enableLinkClicksCapture(plausible, {

  • eventName: 'Link click', // default
  • captureText: true, // include link text as a prop
  • filter: (url, text) => url.startsWith('https://'),
    +});
    +```

+## Score user sessions
+
+To collect device/region signals and detect bots, call enableSessionScoring:
+
+```ts
+import { Plausible, enableSessionScoring } from 'plausible-client';
+
+const plausible = new Plausible({

+// Sends a "Session scored" event on the next animation frame
+enableSessionScoring(plausible);
+ + +The `Session scored` event includes these props: + +| Prop | Description | +|---|---| +| `botScore` | Numeric score — ≥ 3 indicates a likely bot | +| `botSignals` | Comma-separated triggered signals, e.g. `webdriver,headless_ua` | +| `sessionAge` | Seconds since the first recorded visit | +| `timeZone` | IANA timezone resolved from `Intl.DateTimeFormat` | +| `language` | `navigator.language` | +| `languages` | `navigator.languages` joined with `,` | +| `screenSize` | `{width}x{height}` from `window.screen` | +| `hardwareConcurrency` | Number of logical CPU cores | +| `deviceMemory` | Device RAM in GB (where available) | +| `devicePixelRatio` | Screen pixel density | + +You can customise the storage backend or key used to persist the first-visit timestamp: + +ts
+import { Plausible, enableSessionScoring, CookieStorage } from 'plausible-client';
+
+enableSessionScoring(plausible, {

  • storage: new CookieStorage(),
  • firstVisitKey: 'my_first_visit',
    +});
    +```

+You can also use getBotSignals() independently:
+
+ts +import { getBotSignals } from 'plausible-client'; + +const { isBot, score, signals } = getBotSignals(); +// isBot: boolean, score: number, signals: string[] +
+

Filter events

@@ -155,7 +197,7 @@ const plausible = new Plausible({
transform: userId({
storage: new BrowserUIDStorage({
// Store UID in JS cookies

  •  store: CookieStorage(),
    
  •  store: new CookieStorage(),
     // Customize storage key to persist ID
     key: 'uid'
    
    }),

**Summary of changes:**
- **`enableAutoOutboundTracking`** — adds a note that same-origin links are skipped, and shows the optional `captureText`/`filter` config.
- **`enableLinkClicksCapture`** — new section with full config example.
- **`enableSessionScoring`** — new section with prop table, storage customisation, and standalone `getBotSignals()` usage.
- **`CookieStorage()` → `new CookieStorage()`** — fixes the existing `userId` example to match the class-based implementation.

@vitonsky vitonsky merged commit 5a2b923 into master Mar 8, 2026
2 checks passed
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.

Score user sessions

1 participant