You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build:microbundle per-package. Core has split client/ and server/ builds; everything else is a single bundle.
Tests:uvu (newer packages) and ava (legacy at root).
Types today are produced by an unusual two-step pipeline in packages/analytics-core/scripts/types.cjs:1:
tsd-jsdoc walks JSDoc in src/ and emits _temp-types/types.d.ts.
The script then regex-rewrites the output — converting type AnalyticsInstance = to interface, splicing <T> generics into *Hook / *Context / *ContextProps types, renaming AnalyticsPlugin → AnalyticsPluginBase and re-exporting it as AnalyticsPlugin<T extends string = string>, etc. — to ship dist/types.d.ts.
A few util packages already use TypeScript-via-allowJs: analytics-util-types, analytics-utils, analytics-util-listener, analytics-util-activity, analytics-util-url, analytics-util-control-flow, analytics-util-visitor-source. Each has a minimal tsconfig.json with allowJs: true + emitDeclarationOnly: true. No .ts source files exist yet — these still author in JS and only emit declarations.
Plugin contract lives in two places: a tiny JSDoc @typedef in packages/analytics-core/src/pluginTypeDef.js:1 (just name, EVENTS, config, initialize, page, track, identify, loaded, ready) and the runtime parser in packages/analytics-core/src/index.js:65 which treats any plugin key that isn't in nonEvents = ['name', 'EVENTS', 'config', 'loaded'] as a hook handler. So plugins can register arbitrary lifecycle and custom event handlers.
Core lifecycle events are listed in packages/analytics-core/src/events.js:3 (~30 events: bootstrap, page/pageStart/pageEnd/pageAborted, track*, identify*, reset*, setItem*/removeItem*, online/offline, plus dynamic registerPlugin:<name> and ready:<name>).
Plugins export a factory that returns a plain object of method handlers receiving { payload, config, instance, plugins, abort } — see packages/analytics-plugin-google-analytics/src/browser.js:81.
The regex-rewrite type pipeline is brittle, tells the real story: David already wants generics on plugins, hooks, and payloads — the current setup is a workaround for not having TS source. A real migration replaces this with first-class .ts.
Goals
Author packages in TypeScript directly (no more JSDoc → tsd-jsdoc → regex).
Ship strong types with generics so consumers can:
Declare a custom event map and get analytics.track('MyEvent', { ... }) autocomplete + payload checking.
Declare custom plugin methods and get analytics.plugins.myPlugin.doThing() typed.
Get typed payload in .on('track', ({ payload }) => ...) based on the event name.
Keep zero‑runtime‑change guarantee — types only, no behavioral churn.
Stay backwards compatible for JS consumers and existing plugin authors.
Migrate incrementally, package‑by‑package, with green tests at every step.
Bump typescript from ^4.3.5 (root) to ^5.4 so we can use satisfies, const type params, and Bundler resolution — all useful for the generic plugin system.
Add TS Project References so analytics-core depends on analytics-utils, etc. — enables tsc --build for fast incremental type-check across the workspace.
Replace microbundle for .ts packages with tsup (esbuild-based, supports CJS+ESM+.d.ts in one shot, watch mode). Keep microbundle for any package not yet migrated. Both can coexist during the rollout.
Add a typecheck turbo task wired into CI.
Phase 1 — Define the public type surface in a new package
Create packages/analytics-types (or fold into existing @analytics/type-utils) containing the canonical .ts definitions for everything dist/types.d.ts ships today, but rewritten as native generics:
// Core event registry — extensible via module augmentationexportinterfaceCoreEventMap{bootstrap: {params: Record<string,string>;user: User;persistedUser: User;initialUser: Partial<User>}page: {properties: PageData;options: EventOptions;userId: string|null;anonymousId: string}track: {event: string;properties: Record<string,unknown>;options: EventOptions;userId: string|null;anonymousId: string}identify: {userId: string|null;traits: Record<string,unknown>;options: EventOptions;anonymousId: string;previousId?: string}// ...full list mirrored from src/events.js}// Allow user augmentation: declare module '@analytics/types' { interface CoreEventMap { 'cart:abandoned': { items: string[] }}}exporttypeEventName=keyofCoreEventMapexporttypePayloadOf<EextendsEventName>=CoreEventMap[E]exportinterfaceHookContext<EextendsEventName,Cfg=Record<string,unknown>>{payload: CoreEventMap[E]config: Cfginstance: AnalyticsInstanceplugins: PluginsStateabort: (reason?: string)=>void}// Plugin definition is generic over its config and the methods it addsexportinterfaceAnalyticsPlugin<Nameextendsstring=string,Cfg=Record<string,unknown>,MethodsextendsRecord<string,(...a: any[])=>any>={},EventsextendsRecord<string,string>={},>{name: NameEVENTS?: Eventsconfig?: Cfgenabled?: booleanloaded?: (ctx: {config: Cfg})=>booleaninitialize?: (ctx: HookContext<'initialize',Cfg>)=>void|Promise<void>page?: (ctx: HookContext<'page',Cfg>)=>void|Promise<void>track?: (ctx: HookContext<'track',Cfg>)=>void|Promise<void>identify?: (ctx: HookContext<'identify',Cfg>)=>void|Promise<void>ready?: (ctx: HookContext<'ready',Cfg>)=>void|Promise<void>reset?: (ctx: HookContext<'reset',Cfg>)=>void|Promise<void>methods?: Methods// open-ended so plugins can hook custom events[hook: string]: unknown}
The instance becomes generic over the union of plugins so custom methods land on analytics.plugins:
This phase is types only, shippable independently — JS consumers get smarter .d.ts immediately, and plugin authors can start targeting the new shapes. Land it before any source rewrites.
Phase 2 — Migrate analytics-utils and the small util packages (low risk)
Convert each .js → .ts one util at a time. Most of these are 1–3 files.
Drop allowJs: true; switch builds to tsup.
Verify the existing uvu tests still run (uvu supports .ts via tsm or tsx loader — add tsm as devDep).
Order: analytics-util-types, analytics-utils, analytics-util-storage, analytics-util-listener, analytics-util-activity, analytics-util-url, analytics-util-params, analytics-util-queue, analytics-util-router, then the rest.
Phase 3 — Migrate analytics-core (the big one)
Convert src/index.js (1,032 lines) and the modules/, middleware/, utils/ subtrees to .ts.
Deletescripts/types.cjs and the tsd-jsdoc regex pipeline. tsc now emits the declarations directly.
Internally type the redux-style store actions as a discriminated union on action.type keyed by CoreEventMap — this gives compile-time exhaustiveness on every reducer and middleware.
Keep the client/ and server/ build split. The TS source is the same; only the bundler defines (BROWSER, SERVER, NODE_ENV) differ. tsup handles that with define.
Tests in packages/analytics-core/tests/ stay as .js for now — they're integration tests against the public API, no need to convert. (Optional follow-up.)
Phase 4 — Migrate plugins
Plugins are independent — convert in any order, or open it up to community PRs after Phase 3 lands.
Each plugin gets a typed Config interface and uses defineAnalyticsPlugin<Config, Methods, Events>({...}) (a tiny helper that just returns its argument but pins generics):
Suggested order: ones already using TypeScript-flavored utils first (analytics-plugin-do-not-track, analytics-plugin-tab-events, analytics-plugin-window-events), then the heavier ones (google-analytics, segment, mixpanel, hubspot, intercom, fullstory, etc.).
Phase 5 — Migrate the wrapper packages and tooling
packages/analytics/src/index.js is 16 lines — trivial.
analytics-cli, gatsby-plugin-analytics: convert and ship typed.
Update examples/ README snippets to show the generic event-map augmentation pattern.
Phase 6 — Cleanup
Remove tsd-jsdoc, dox, doxxx, jsdoc, jsdoc-to-markdown from devDeps if doc generation can switch to typedoc (recommended — reads .ts natively).
Site (site/) docs generation likely consumes the JSDoc output; rewire to typedoc --json or keep the existing JSDoc comments above each TS export (TypeDoc reads both).
Drop babel config if unused after migration.
What "generic types to dynamically support plugin methods and custom events" looks like for users
// User projectimport{Analytics}from'analytics'importgoogleAnalyticsfrom'@analytics/google-analytics'importcustomerIofrom'@analytics/customerio'// 1. Augment the event map with custom eventsdeclare module '@analytics/types'{interfaceCoreEventMap{'cart:abandoned': {items: string[];cartValue: number}'video:watched': {videoId: string;durationMs: number}}}constanalytics=Analytics({app: 'my-app',plugins: [googleAnalytics({measurementIds: ['G-X']}),customerIo({siteId: 'abc'})]asconst,})analytics.track('cart:abandoned',{items: ['a'],cartValue: 99})// ✓ typedanalytics.track('cart:abandoned',{items: ['a']})// ✗ missing cartValueanalytics.track('unknown:event',{})// ✗ unless map is augmentedanalytics.on('video:watched',({ payload })=>{payload.durationMs.toFixed(2)// ✓ typed})// 2. Plugin methods land on .plugins.<name>analytics.plugins['customerio'].identifyAnonymous(...)// ✓ typed (if customerIo declares methods)
The two extension points are module augmentation (for events, opt-in, no breaking change for existing users) and as const plugin tuples (for plugin methods, inferred automatically).
Risks & open questions
Bundle size:tsup/esbuild output is competitive with microbundle for ESM but the legacy UMD/IIFE outputs (used by analytics-core/client/build:standalone) need verification. May need to keep rollup for the CDN bundle.
Backward compat for JS plugin authors: the runtime accepts [hook: string]: unknown on a plugin, so existing JS plugins keep working — only the types tighten. Worth documenting.
AnalyticsInstance<P> ergonomics: consumers who store the instance in a non-inferred location (e.g. React context) will need ReturnType<typeof Analytics<typeof plugins>> or a helper alias. Worth shipping a type AnalyticsClient<P> = ... alias and doc snippet.
Doc pipeline: the current site reads JSDoc-derived markdown. Decide upfront: keep JSDoc comments on TS exports + jsdoc-to-markdown, or move to TypeDoc. (My recommendation: TypeDoc; outputs richer docs and avoids dual-sourcing.)
Test runner:uvu works with TS via tsm. The legacy root ava config (packages/**/*.test.js) should be retired once everything's on uvu.
Estimated scope: Phase 0–1 is ~1–2 weeks elapsed; Phase 2 (~15 utils) is mechanical, ~1 week; Phase 3 (analytics-core) is the hard one — 2–3 weeks for the rewrite + verifying both client and server builds + ensuring the discriminated-union action types don't regress runtime; Phase 4 (~30 plugins) is parallelizable and could absorb community help over a release cycle.
Want me to start with Phase 0 (root tsconfig.base.json, TS bump to 5.4, project references skeleton) as the first concrete PR? That's the smallest change that unblocks everything else and is safe to land independently.
• Branch: claude/issue-513-20260502-0335
Activity
github-actions commentedon May 2, 2026
📋 Claude Code execution complete ✅
ℹ️ Analysis only (no code changes)
📊 Execution Info: Issue #513 | Unknown (403 files) | @DavidWells
ℹ️ Action Run: https://github.com/DavidWells/analytics/actions/runs/25242829112
🚀 Automated management executed:
github-actions commentedon May 2, 2026
Claude finished @DavidWells's task in 3m 22s —— View job
TypeScript Migration Plan
packages/analytics-core)Where the codebase is today
analytics,analytics-core,analytics-utils, ~30 plugins (analytics-plugin-*), ~15 utils (analytics-util-*), plusgatsby-plugin-analyticsandanalytics-cli.microbundleper-package. Core has splitclient/andserver/builds; everything else is a single bundle.uvu(newer packages) andava(legacy at root).packages/analytics-core/scripts/types.cjs:1:tsd-jsdocwalks JSDoc insrc/and emits_temp-types/types.d.ts.type AnalyticsInstance =tointerface, splicing<T>generics into*Hook/*Context/*ContextPropstypes, renamingAnalyticsPlugin→AnalyticsPluginBaseand re-exporting it asAnalyticsPlugin<T extends string = string>, etc. — to shipdist/types.d.ts.allowJs:analytics-util-types,analytics-utils,analytics-util-listener,analytics-util-activity,analytics-util-url,analytics-util-control-flow,analytics-util-visitor-source. Each has a minimaltsconfig.jsonwithallowJs: true+emitDeclarationOnly: true. No.tssource files exist yet — these still author in JS and only emit declarations.JSDoc @typedefinpackages/analytics-core/src/pluginTypeDef.js:1(justname,EVENTS,config,initialize,page,track,identify,loaded,ready) and the runtime parser inpackages/analytics-core/src/index.js:65which treats any plugin key that isn't innonEvents = ['name', 'EVENTS', 'config', 'loaded']as a hook handler. So plugins can register arbitrary lifecycle and custom event handlers.packages/analytics-core/src/events.js:3(~30 events:bootstrap,page/pageStart/pageEnd/pageAborted,track*,identify*,reset*,setItem*/removeItem*,online/offline, plus dynamicregisterPlugin:<name>andready:<name>).{ payload, config, instance, plugins, abort }— seepackages/analytics-plugin-google-analytics/src/browser.js:81.The regex-rewrite type pipeline is brittle, tells the real story: David already wants generics on plugins, hooks, and payloads — the current setup is a workaround for not having TS source. A real migration replaces this with first-class
.ts.Goals
analytics.track('MyEvent', { ... })autocomplete + payload checking.analytics.plugins.myPlugin.doThing()typed.payloadin.on('track', ({ payload }) => ...)based on the event name.Proposed phased plan
Phase 0 — Foundations (1 PR, no source migration)
tsconfig.base.jsonwith:target: ES2019,module: ESNext,moduleResolution: Bundler,strict: true,declaration: true,declarationMap: true,sourceMap: true,isolatedModules: true,esModuleInterop: true,skipLibCheck: true,forceConsistentCasingInFileNames: true,resolveJsonModule: true.typescriptfrom^4.3.5(root) to^5.4so we can usesatisfies,consttype params, andBundlerresolution — all useful for the generic plugin system.analytics-coredepends onanalytics-utils, etc. — enablestsc --buildfor fast incremental type-check across the workspace.microbundlefor.tspackages withtsup(esbuild-based, supports CJS+ESM+.d.tsin one shot, watch mode). Keepmicrobundlefor any package not yet migrated. Both can coexist during the rollout.typecheckturbo task wired into CI.Phase 1 — Define the public type surface in a new package
Create
packages/analytics-types(or fold into existing@analytics/type-utils) containing the canonical.tsdefinitions for everythingdist/types.d.tsships today, but rewritten as native generics:The instance becomes generic over the union of plugins so custom methods land on
analytics.plugins:This phase is types only, shippable independently — JS consumers get smarter
.d.tsimmediately, and plugin authors can start targeting the new shapes. Land it before any source rewrites.Phase 2 — Migrate
analytics-utilsand the small util packages (low risk).js→.tsone util at a time. Most of these are 1–3 files.allowJs: true; switch builds totsup.uvutests still run (uvu supports.tsviatsmortsxloader — addtsmas devDep).Order:
analytics-util-types,analytics-utils,analytics-util-storage,analytics-util-listener,analytics-util-activity,analytics-util-url,analytics-util-params,analytics-util-queue,analytics-util-router, then the rest.Phase 3 — Migrate
analytics-core(the big one)src/index.js(1,032 lines) and themodules/,middleware/,utils/subtrees to.ts.scripts/types.cjsand thetsd-jsdocregex pipeline.tscnow emits the declarations directly.action.typekeyed byCoreEventMap— this gives compile-time exhaustiveness on every reducer and middleware.client/andserver/build split. The TS source is the same; only the bundler defines (BROWSER,SERVER,NODE_ENV) differ. tsup handles that withdefine.packages/analytics-core/tests/stay as.jsfor now — they're integration tests against the public API, no need to convert. (Optional follow-up.)Phase 4 — Migrate plugins
Plugins are independent — convert in any order, or open it up to community PRs after Phase 3 lands.
Each plugin gets a typed
Configinterface and usesdefineAnalyticsPlugin<Config, Methods, Events>({...})(a tiny helper that just returns its argument but pins generics):Suggested order: ones already using TypeScript-flavored utils first (
analytics-plugin-do-not-track,analytics-plugin-tab-events,analytics-plugin-window-events), then the heavier ones (google-analytics,segment,mixpanel,hubspot,intercom,fullstory, etc.).Phase 5 — Migrate the wrapper packages and tooling
packages/analytics/src/index.jsis 16 lines — trivial.analytics-cli,gatsby-plugin-analytics: convert and ship typed.examples/README snippets to show the generic event-map augmentation pattern.Phase 6 — Cleanup
tsd-jsdoc,dox,doxxx,jsdoc,jsdoc-to-markdownfrom devDeps if doc generation can switch totypedoc(recommended — reads.tsnatively).site/) docs generation likely consumes the JSDoc output; rewire totypedoc --jsonor keep the existing JSDoc comments above each TS export (TypeDoc reads both).babelconfig if unused after migration.What "generic types to dynamically support plugin methods and custom events" looks like for users
The two extension points are module augmentation (for events, opt-in, no breaking change for existing users) and
as constplugin tuples (for plugin methods, inferred automatically).Risks & open questions
tsup/esbuild output is competitive withmicrobundlefor ESM but the legacy UMD/IIFE outputs (used byanalytics-core/client/build:standalone) need verification. May need to keeprollupfor the CDN bundle.[hook: string]: unknownon a plugin, so existing JS plugins keep working — only the types tighten. Worth documenting.AnalyticsInstance<P>ergonomics: consumers who store the instance in a non-inferred location (e.g. React context) will needReturnType<typeof Analytics<typeof plugins>>or a helper alias. Worth shipping atype AnalyticsClient<P> = ...alias and doc snippet.uvuworks with TS viatsm. The legacy rootavaconfig (packages/**/*.test.js) should be retired once everything's on uvu.analytics-core) is the hard one — 2–3 weeks for the rewrite + verifying both client and server builds + ensuring the discriminated-union action types don't regress runtime; Phase 4 (~30 plugins) is parallelizable and could absorb community help over a release cycle.Want me to start with Phase 0 (root
tsconfig.base.json, TS bump to 5.4, project references skeleton) as the first concrete PR? That's the smallest change that unblocks everything else and is safe to land independently.• Branch:
claude/issue-513-20260502-0335