Skip to content

new: analytics package for Solid 2.0#925

Open
davedbase wants to merge 9 commits into
solidjs-community:nextfrom
davedbase:v2/analytics
Open

new: analytics package for Solid 2.0#925
davedbase wants to merge 9 commits into
solidjs-community:nextfrom
davedbase:v2/analytics

Conversation

@davedbase
Copy link
Copy Markdown
Member

@davedbase davedbase commented Jun 1, 2026

Adds a new analytics package providing a queue-based analytics dispatch pipeline for Solid 2.0, with full SSR support, reactive signals, context sharing, navigation guards, and server relay. This should be considered an experimental package because it's trying something pretty unique. It needs a lot of work.

I'm thinking of removing or rethinking the leave guard.

What's included

Core primitives

  • makeAnalytics(plugins, options?) — non-reactive base primitive; returns [controls, cleanup]. Works at module level, in server functions, anywhere.
  • createAnalytics(plugins, options?) — reactive wrapper; auto-disposes with the owner, exposes pendingCount: Accessor<number> tracking queued events while plugins initialize.
  • AnalyticsProvider / useAnalytics() — context pair for sharing a single analytics instance across the component tree.

Navigation guards

  • makeAnalyticsGuard / createAnalyticsGuard — pauses SolidJS Router navigation until all in-flight dispatches settle, preventing events from being lost on route change. Also registers a beforeunload safety net.

Server relay

  • createServerPlugin (/relay) — client-side plugin that forwards events to a server function, keeping provider API keys off the client.
  • createSolidStartRelayPlugin (/relay/solidstart) — thin wrapper for wiring to a SolidStart action.
  • createTanStackRelayPlugin (/relay/tanstack) — wraps a TanStack Start createServerFn, handling the { data } call signature automatically.

Design decisions

  • Plugin interface is compatible with the analytics npm package — any existing plugin works without modification.
  • Events queue in a bounded FIFO when plugins are still initializing and replay in order once all plugins are ready.
  • Dispatch is sequential per plugin list; any plugin can call abort() to stop the chain for that event.
  • createAnalytics delegates to makeAnalytics via an optional onQueueChange callback — no logic duplication.
  • Uses @solid-primitives/queue's makeQueue internally for the event buffer.
  • Fully isomorphic — page() omits window-dependent defaults on the server; guards are no-ops in SSR.

Summary by CodeRabbit

Release Notes

  • New Features

    • Plugin-based analytics system with event queueing and automatic replay
    • Context provider API for sharing analytics across components
    • Navigation guards to ensure pending events are flushed before route transitions
    • Server relay plugins for SolidStart and TanStack Start integration
    • Configurable batched event draining for optimized performance
  • Documentation

    • Complete migration guide from previous API
    • Comprehensive documentation with usage examples and SSR behavior
  • Chores

    • Package bumped to version 1.0.0
    • Updated to support Solid.js v2.0 beta

@davedbase davedbase added this to the Solid 2.0 Migration milestone Jun 1, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

🦋 Changeset detected

Latest commit: dd15163

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/analytics Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 32ffeca3-fd94-4a24-8f52-47d6e773f4ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@davedbase davedbase changed the base branch from main to next June 1, 2026 16:22
@davedbase davedbase marked this pull request as ready for review June 1, 2026 16:22
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: 9

🧹 Nitpick comments (1)
packages/analytics/src/guard.ts (1)

19-30: ⚡ Quick win

Navigation can stall indefinitely if drain() never settles.

onBeforeLeave prevents navigation and only retries inside drain().finally(...). If any in-flight plugin promise hangs (e.g. a network request without its own timeout), drain() never settles and the user is permanently blocked from navigating. Consider racing drain() against a bounded timeout so navigation always proceeds:

♻️ Suggested approach
-    void analytics.drain().finally(() => {
+    const timeout = new Promise<void>(r => setTimeout(r, 2000));
+    void Promise.race([analytics.drain(), timeout]).finally(() => {
       guarding = false;
       event.retry(true);
     });
🤖 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 `@packages/analytics/src/guard.ts` around lines 19 - 30, The onBeforeLeave
handler in buildOnBeforeLeave currently blocks navigation until
analytics.drain() settles, which can hang indefinitely; modify onBeforeLeave to
race analytics.drain() with a bounded timeout (e.g.,
Promise.race([analytics.drain(), timeoutPromise])) and ensure the finally-like
cleanup always runs: clear guarding and call event.retry(true) after the race
regardless of which wins, and cancel/prevent duplicate retries by keeping the
guarding flag logic around onBeforeLeave and the returned promise resolution
path.
🤖 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 `@packages/analytics/package.json`:
- Around line 54-63: primitive.list in packages/analytics/package.json is out of
sync: remove the non-existent "createEventHandler" entry and add the actual
relay plugin primitives "createSolidStartRelayPlugin" and
"createTanStackRelayPlugin" (these correspond to the ./relay/solidstart and
./relay/tanstack exports); update the "list" array so it matches the real
exported API names (e.g., keep "makeAnalytics", "createAnalytics",
"AnalyticsProvider", "useAnalytics", "makeAnalyticsGuard",
"createAnalyticsGuard", "createServerPlugin" and replace "createEventHandler"
with "createSolidStartRelayPlugin" and "createTanStackRelayPlugin") so subpath
exports and primitive.list stay consistent.

In `@packages/analytics/README.md`:
- Around line 189-191: The fenced code block containing "browser →
createServerPlugin → your server function → GA / Mixpanel / Segment" is missing
a language identifier and triggers markdownlint MD040; update that markdown
fence to include a language (e.g., change ``` to ```text) so the block is
explicitly marked as text.

In `@packages/analytics/src/analytics.ts`:
- Around line 105-113: drainBatch currently applies drainSize unconditionally
which causes leftover events when batching is disabled; change logic so
drainSize is only used when batching is true — i.e., in drainBatch (or its
caller drainQueue) check the batching flag and set max = batching ? (drainSize
?? Infinity) : Infinity (or skip using drainSize entirely when batching is
false) so that initPlugin/startPoll draining (called when allReady) will remove
the entire queue (using queue.remove) and not leave events stranded before
dispatch bypasses the queue.

In `@packages/analytics/src/context.ts`:
- Around line 34-45: The useAnalytics hook currently returns
useContext(AnalyticsCtx) directly which can be undefined when no provider
exists, making the ReactiveAnalyticsControls return type unsound and
contradicting the JSDoc; change useAnalytics to call useContext(AnalyticsCtx),
store the result in a local (e.g., controls), if controls is undefined throw
ContextNotFoundError (or construct a helpful error message) and otherwise return
controls so the runtime matches the declared ReactiveAnalyticsControls type and
the documented behavior; reference the useAnalytics function, AnalyticsCtx,
ReactiveAnalyticsControls, and ContextNotFoundError when making this change.

In `@packages/analytics/src/guard.ts`:
- Around line 38-43: The JSDoc example redeclares cleanup in the same scope;
change one of the identifiers to avoid collision (e.g., rename the cleanup
returned from makeAnalyticsGuard to cleanupGuard or guardCleanup) so the example
compiles; update the example lines that destructure makeAnalytics and
makeAnalyticsGuard (referencing makeAnalytics and makeAnalyticsGuard) and any
subsequent usage to use the new name.

In `@packages/analytics/src/relay/solidstart.ts`:
- Around line 11-29: The documented SolidStart `action` example (relayEvent)
returns Promise<void>, which triggers Solid Router page query revalidation;
update the example in the docs to explicitly opt out of revalidation by
returning a response with revalidate: [] (e.g., return json(..., { revalidate:
[] }) or return reload({ revalidate: [] })) from the action after awaiting
ga.track, and also relax the typings on createSolidStartRelayPlugin /
createServerPlugin to accept broader return types (e.g., Promise<unknown> or
Promise<any>) instead of Promise<void> so the example type-checks.

In `@packages/analytics/src/relay/tanstack.ts`:
- Around line 11-30: Update the JSDoc example so the Solid-specific import and
validation API are correct: change the import of createServerFn to come from
"`@tanstack/solid-start`" instead of "`@tanstack/start`", and replace the chained
.validator(...) call with .inputValidator(...) on the createServerFn returned
object (referenced as createServerFn and relayEvent in the example); keep the
rest of the snippet (AnyPayload, googleAnalytics, createTanStackRelayPlugin)
unchanged so the example is copy-paste safe for Solid users.

In `@packages/analytics/test/index.test.ts`:
- Around line 12-14: Tests in packages/analytics/test/index.test.ts rely on real
timers via the delay() helper which makes CI flaky; switch to Vitest fake timers
by calling vi.useFakeTimers() at the start of each test (and vi.useRealTimers()
in teardown, or restore timers in existing afterEach(() => dispose?.()) blocks)
and replace all uses of await delay(n) or delay(n).then(...) with await
vi.advanceTimersByTimeAsync(n) (use await vi.advanceTimersByTimeAsync(0) where
delay() was called with no arg); update the listed waits
(23,36,51,74,78,89,101,116,129,146,160,163,174,211,276,311,314,329,333,349,365,396,422)
accordingly and ensure any setInterval/poll/drain logic in analytics.ts runs by
advancing timers instead of real waiting.

In `@packages/utils/src/index.ts`:
- Around line 105-109: The docs/typing overstate global uniqueness: update the
EventMeta.rid description and any README text to say the ID from
createIdGenerator()/generateId is unique per analytics instance/generator (not
globally across separate clients/SSR requests); alternatively, to harden
cross-client uniqueness modify createIdGenerator to append a small random
segment (e.g., hex or base36 random 4-8 chars via crypto or Math.random) to the
`${Date.now().toString(36)}-${(++seq).toString(36)}` output so rid becomes
`${ts}-${seq}-${rand}`, and ensure any references to generateId in
packages/analytics/src/analytics.ts and the EventMeta type/comment are updated
to reflect the change.

---

Nitpick comments:
In `@packages/analytics/src/guard.ts`:
- Around line 19-30: The onBeforeLeave handler in buildOnBeforeLeave currently
blocks navigation until analytics.drain() settles, which can hang indefinitely;
modify onBeforeLeave to race analytics.drain() with a bounded timeout (e.g.,
Promise.race([analytics.drain(), timeoutPromise])) and ensure the finally-like
cleanup always runs: clear guarding and call event.retry(true) after the race
regardless of which wins, and cancel/prevent duplicate retries by keeping the
guarding flag logic around onBeforeLeave and the returned promise resolution
path.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 638db1dc-28a2-4642-9265-1187c59e3946

📥 Commits

Reviewing files that changed from the base of the PR and between 1606d42 and 018be52.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .changeset/analytics-solid2-redesign.md
  • packages/analytics/README.md
  • packages/analytics/package.json
  • packages/analytics/src/analytics.ts
  • packages/analytics/src/context.ts
  • packages/analytics/src/ga.ts
  • packages/analytics/src/guard.ts
  • packages/analytics/src/index.ts
  • packages/analytics/src/relay.ts
  • packages/analytics/src/relay/solidstart.ts
  • packages/analytics/src/relay/tanstack.ts
  • packages/analytics/src/types.ts
  • packages/analytics/stories/analytics.stories.tsx
  • packages/analytics/test/index.test.ts
  • packages/analytics/test/server.test.ts
  • packages/analytics/tsconfig.json
  • packages/queue/src/task-queue.ts
  • packages/utils/src/index.ts
💤 Files with no reviewable changes (1)
  • packages/analytics/src/ga.ts

Comment thread packages/analytics/package.json
Comment thread packages/analytics/README.md Outdated
Comment thread packages/analytics/src/analytics.ts
Comment thread packages/analytics/src/context.ts Outdated
Comment thread packages/analytics/src/guard.ts
Comment thread packages/analytics/src/relay/solidstart.ts
Comment thread packages/analytics/src/relay/tanstack.ts
Comment thread packages/analytics/test/index.test.ts Outdated
Comment thread packages/utils/src/index.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant