@clamp-sh/analytics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @clamp-sh/analytics
2
+
3
+ Analytics SDK for [Clamp](https://clamp.sh), agentic analytics your coding agent can read, query, and act on. Track pageviews, custom events, and server-side actions. No cookies, no personal data collected, no consent banner required.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @clamp-sh/analytics
9
+ ```
10
+
11
+ ## Browser
12
+
13
+ ```ts
14
+ import { init, track, getAnonymousId } from "@clamp-sh/analytics"
15
+
16
+ init("proj_xxx")
17
+
18
+ // Custom events
19
+ track("signup", { plan: "pro" })
20
+
21
+ // Get visitor ID (for linking server-side events)
22
+ const anonId = getAnonymousId()
23
+ ```
24
+
25
+ After `init()`, pageviews are tracked automatically, including SPA navigations.
26
+
27
+ ## React
28
+
29
+ ```tsx
30
+ import { Analytics } from "@clamp-sh/analytics/react"
31
+
32
+ export default function RootLayout({ children }) {
33
+ return (
34
+ <html>
35
+ <body>
36
+ {children}
37
+ <Analytics projectId="proj_xxx" />
38
+ </body>
39
+ </html>
40
+ )
41
+ }
42
+ ```
43
+
44
+ Add to your root layout. Pageviews are tracked automatically. Use `track()` from `@clamp-sh/analytics` anywhere in your app for custom events.
45
+
46
+ ## Server
47
+
48
+ ```ts
49
+ import { init, track } from "@clamp-sh/analytics/server"
50
+
51
+ init({ projectId: "proj_xxx", apiKey: "sk_proj_..." })
52
+
53
+ await track("account_created", {
54
+ anonymousId: "anon_abc123",
55
+ properties: { plan: "pro" },
56
+ })
57
+ ```
58
+
59
+ Server events require an API key (found in your project settings).
60
+
61
+ ## Script tag
62
+
63
+ ```html
64
+ <script src="https://cdn.clamp.sh/v1.js"></script>
65
+ <script>
66
+ clamp.init("proj_xxx")
67
+ </script>
68
+ ```
69
+
70
+ ## Custom events
71
+
72
+ Track any action with `track(name, properties)`. Properties are flat string key-value pairs.
73
+
74
+ ```ts
75
+ import { track } from "@clamp-sh/analytics"
76
+
77
+ track("signup", { plan: "pro", source: "pricing_page" })
78
+ track("feature_used", { name: "csv_export" })
79
+ track("invite_sent", { role: "editor", team: "acme" })
80
+ ```
81
+
82
+ On the server:
83
+
84
+ ```ts
85
+ import { track } from "@clamp-sh/analytics/server"
86
+
87
+ await track("subscription_created", {
88
+ anonymousId: "anon_abc123",
89
+ properties: { plan: "pro", interval: "monthly" },
90
+ })
91
+ ```
92
+
93
+ Pageviews are tracked automatically. Everything else goes through `track()`.
94
+
95
+ ## Typed events
96
+
97
+ Define your event map once and get autocomplete and type checking across your app. Zero runtime cost.
98
+
99
+ ```ts
100
+ type Events = {
101
+ signup: { plan: string; source: string }
102
+ purchase: { amount: string; currency: string }
103
+ feature_used: { name: string }
104
+ invite_sent: { role: string }
105
+ }
106
+
107
+ init<Events>("proj_xxx")
108
+
109
+ track("signup", { plan: "pro", source: "homepage" }) // autocomplete
110
+ track("signup", { wrong: "field" }) // type error
111
+ track("unknown_event") // type error
112
+ ```
113
+
114
+ Works the same way with the server SDK:
115
+
116
+ ```ts
117
+ import { init, track } from "@clamp-sh/analytics/server"
118
+
119
+ init<Events>({ projectId: "proj_xxx", apiKey: "sk_proj_..." })
120
+
121
+ await track("purchase", {
122
+ properties: { amount: "49", currency: "usd" },
123
+ })
124
+ ```
125
+
126
+ ## Properties limits
127
+
128
+ Event properties are flat `string → string` maps. Nested objects, arrays, and non-string values are rejected.
129
+
130
+ | Constraint | Limit |
131
+ | -------------- | -------- |
132
+ | Max keys | 20 |
133
+ | Key length | 128 chars |
134
+ | Value length | 512 chars |
135
+ | Payload size | 64 KB |
136
+
137
+ ## Examples
138
+
139
+ ### Track signups with plan info
140
+
141
+ ```ts
142
+ track("signup", { plan: "pro", source: "pricing_page" })
143
+ ```
144
+
145
+ ### Track feature usage
146
+
147
+ ```ts
148
+ track("feature_used", { name: "csv_export" })
149
+ ```
150
+
151
+ ### Track button clicks
152
+
153
+ ```tsx
154
+ <button onClick={() => track("cta_clicked", { label: "Get started", page: "/pricing" })}>
155
+ Get started
156
+ </button>
157
+ ```
158
+
159
+ ### Link browser visitor to server events
160
+
161
+ Pass the anonymous ID from the browser to your API, then include it in server-side events to connect the two.
162
+
163
+ ```ts
164
+ // Browser: send anonymous ID with your API call
165
+ const anonId = getAnonymousId()
166
+ await fetch("/api/checkout", {
167
+ method: "POST",
168
+ body: JSON.stringify({ plan: "pro", anonId }),
169
+ })
170
+ ```
171
+
172
+ ```ts
173
+ // Server: include it in the event
174
+ await track("checkout_completed", {
175
+ anonymousId: req.body.anonId,
176
+ properties: { plan: "pro", amount: "49" },
177
+ })
178
+ ```
179
+
180
+ ### Track form submissions
181
+
182
+ ```tsx
183
+ function ContactForm() {
184
+ const handleSubmit = (e: FormEvent) => {
185
+ e.preventDefault()
186
+ track("form_submitted", { form: "contact", page: location.pathname })
187
+ }
188
+ return <form onSubmit={handleSubmit}>...</form>
189
+ }
190
+ ```
191
+
192
+ ### Next.js App Router
193
+
194
+ ```tsx
195
+ // app/layout.tsx
196
+ import { Analytics } from "@clamp-sh/analytics/react"
197
+
198
+ export default function RootLayout({ children }) {
199
+ return (
200
+ <html>
201
+ <body>
202
+ {children}
203
+ <Analytics projectId="proj_xxx" />
204
+ </body>
205
+ </html>
206
+ )
207
+ }
208
+
209
+ // app/pricing/page.tsx (client component)
210
+ "use client"
211
+ import { track } from "@clamp-sh/analytics"
212
+
213
+ export default function Pricing() {
214
+ return (
215
+ <button onClick={() => track("plan_selected", { plan: "growth" })}>
216
+ Choose Growth
217
+ </button>
218
+ )
219
+ }
220
+ ```
221
+
222
+ ### Next.js Server Actions
223
+
224
+ ```ts
225
+ // app/actions.ts
226
+ "use server"
227
+ import { track } from "@clamp-sh/analytics/server"
228
+
229
+ export async function createTeam(name: string, anonId: string) {
230
+ const team = await db.teams.create({ name })
231
+ await track("team_created", {
232
+ anonymousId: anonId,
233
+ properties: { team_id: team.id },
234
+ })
235
+ return team
236
+ }
237
+ ```
238
+
239
+ ### Express / Node.js backend
240
+
241
+ ```ts
242
+ import express from "express"
243
+ import { init, track } from "@clamp-sh/analytics/server"
244
+
245
+ init({ projectId: "proj_xxx", apiKey: "sk_proj_..." })
246
+
247
+ const app = express()
248
+
249
+ app.post("/api/subscribe", async (req, res) => {
250
+ await track("subscription_started", {
251
+ anonymousId: req.body.anonId,
252
+ properties: { plan: req.body.plan },
253
+ })
254
+ res.json({ ok: true })
255
+ })
256
+ ```
257
+
258
+ ## License
259
+
260
+ MIT
@@ -0,0 +1,21 @@
1
+ import { E as EventMap, A as AnyEvents } from './types-Bc30ZeCx.mjs';
2
+
3
+ /**
4
+ * Initialize the browser SDK. Call once at app startup.
5
+ * Starts auto-pageview tracking, session management, and batching.
6
+ */
7
+ declare function init<_E extends EventMap = AnyEvents>(projectId: string, opts?: {
8
+ endpoint?: string;
9
+ }): void;
10
+ /**
11
+ * Track a custom event. Fire-and-forget; events are batched and sent every 5s.
12
+ * If called before init(), events are buffered and flushed when init() runs.
13
+ */
14
+ declare function track<E extends EventMap = AnyEvents, K extends keyof E & string = string>(name: K, properties?: E[K] extends Record<string, string> ? E[K] : Record<string, string>): void;
15
+ /**
16
+ * Get the anonymous visitor ID (persisted in localStorage).
17
+ * Pass this to your server so server-side events can be linked to the visitor.
18
+ */
19
+ declare function getAnonymousId(): string | null;
20
+
21
+ export { AnyEvents, EventMap, getAnonymousId, init, track };
@@ -0,0 +1,21 @@
1
+ import { E as EventMap, A as AnyEvents } from './types-Bc30ZeCx.js';
2
+
3
+ /**
4
+ * Initialize the browser SDK. Call once at app startup.
5
+ * Starts auto-pageview tracking, session management, and batching.
6
+ */
7
+ declare function init<_E extends EventMap = AnyEvents>(projectId: string, opts?: {
8
+ endpoint?: string;
9
+ }): void;
10
+ /**
11
+ * Track a custom event. Fire-and-forget; events are batched and sent every 5s.
12
+ * If called before init(), events are buffered and flushed when init() runs.
13
+ */
14
+ declare function track<E extends EventMap = AnyEvents, K extends keyof E & string = string>(name: K, properties?: E[K] extends Record<string, string> ? E[K] : Record<string, string>): void;
15
+ /**
16
+ * Get the anonymous visitor ID (persisted in localStorage).
17
+ * Pass this to your server so server-side events can be linked to the visitor.
18
+ */
19
+ declare function getAnonymousId(): string | null;
20
+
21
+ export { AnyEvents, EventMap, getAnonymousId, init, track };
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ getAnonymousId: () => getAnonymousId,
24
+ init: () => init,
25
+ track: () => track
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
29
+ var BrowserClient = class {
30
+ constructor() {
31
+ this.projectId = null;
32
+ this.endpoint = DEFAULT_ENDPOINT;
33
+ this.sessionId = null;
34
+ this.anonymousId = null;
35
+ this.queue = [];
36
+ this.preInitQueue = [];
37
+ this.timer = null;
38
+ this.initialized = false;
39
+ }
40
+ init(projectId, opts) {
41
+ if (typeof window === "undefined") return;
42
+ this.projectId = projectId;
43
+ if (opts?.endpoint) this.endpoint = opts.endpoint;
44
+ this.initialized = true;
45
+ this.sessionId = sessionStorage.getItem("clamp_sid") ?? this.newId("ses");
46
+ sessionStorage.setItem("clamp_sid", this.sessionId);
47
+ this.anonymousId = localStorage.getItem("clamp_aid") ?? this.newId("anon");
48
+ localStorage.setItem("clamp_aid", this.anonymousId);
49
+ for (const e of this.preInitQueue) {
50
+ this.track(e.name, e.props);
51
+ }
52
+ this.preInitQueue = [];
53
+ this.pageview();
54
+ const origPushState = history.pushState.bind(history);
55
+ const origReplaceState = history.replaceState.bind(history);
56
+ history.pushState = (...args) => {
57
+ origPushState(...args);
58
+ this.pageview();
59
+ };
60
+ history.replaceState = (...args) => {
61
+ origReplaceState(...args);
62
+ this.pageview();
63
+ };
64
+ window.addEventListener("popstate", () => this.pageview());
65
+ this.timer = setInterval(() => this.flush(), 5e3);
66
+ const onHide = () => this.flush(true);
67
+ document.addEventListener("visibilitychange", () => {
68
+ if (document.visibilityState === "hidden") onHide();
69
+ });
70
+ window.addEventListener("pagehide", onHide);
71
+ }
72
+ track(name, properties) {
73
+ if (!this.initialized) {
74
+ this.preInitQueue.push({ name, props: properties });
75
+ return;
76
+ }
77
+ const event = {
78
+ name,
79
+ url: location.href,
80
+ referrer: document.referrer,
81
+ sessionId: this.sessionId,
82
+ anonymousId: this.anonymousId,
83
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
84
+ screenWidth: window.innerWidth,
85
+ screenHeight: window.innerHeight,
86
+ language: navigator.language,
87
+ platform: "web",
88
+ properties
89
+ };
90
+ this.queue.push(event);
91
+ }
92
+ getAnonymousId() {
93
+ return this.anonymousId;
94
+ }
95
+ // ── Private ─────────────────────────────────────────────────────────
96
+ pageview() {
97
+ this.track("pageview");
98
+ }
99
+ flush(useBeacon = false) {
100
+ if (!this.projectId || this.queue.length === 0) return;
101
+ const events = this.queue.splice(0, 100);
102
+ const payload = { p: this.projectId, events };
103
+ const url = `${this.endpoint}/e/batch`;
104
+ const body = JSON.stringify(payload);
105
+ if (useBeacon && typeof navigator.sendBeacon === "function") {
106
+ navigator.sendBeacon(url, body);
107
+ } else {
108
+ fetch(url, {
109
+ method: "POST",
110
+ headers: { "content-type": "application/json" },
111
+ body,
112
+ keepalive: true
113
+ }).catch(() => {
114
+ });
115
+ }
116
+ if (this.queue.length > 0) this.flush(useBeacon);
117
+ }
118
+ newId(prefix) {
119
+ const rand = Math.random().toString(36).slice(2, 10);
120
+ const ts = Date.now().toString(36);
121
+ return `${prefix}_${ts}${rand}`;
122
+ }
123
+ };
124
+ var client = new BrowserClient();
125
+ function init(projectId, opts) {
126
+ client.init(projectId, opts);
127
+ }
128
+ function track(name, properties) {
129
+ client.track(name, properties);
130
+ }
131
+ function getAnonymousId() {
132
+ return client.getAnonymousId();
133
+ }
134
+ // Annotate the CommonJS export names for ESM import in node:
135
+ 0 && (module.exports = {
136
+ getAnonymousId,
137
+ init,
138
+ track
139
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,112 @@
1
+ // src/index.ts
2
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
3
+ var BrowserClient = class {
4
+ constructor() {
5
+ this.projectId = null;
6
+ this.endpoint = DEFAULT_ENDPOINT;
7
+ this.sessionId = null;
8
+ this.anonymousId = null;
9
+ this.queue = [];
10
+ this.preInitQueue = [];
11
+ this.timer = null;
12
+ this.initialized = false;
13
+ }
14
+ init(projectId, opts) {
15
+ if (typeof window === "undefined") return;
16
+ this.projectId = projectId;
17
+ if (opts?.endpoint) this.endpoint = opts.endpoint;
18
+ this.initialized = true;
19
+ this.sessionId = sessionStorage.getItem("clamp_sid") ?? this.newId("ses");
20
+ sessionStorage.setItem("clamp_sid", this.sessionId);
21
+ this.anonymousId = localStorage.getItem("clamp_aid") ?? this.newId("anon");
22
+ localStorage.setItem("clamp_aid", this.anonymousId);
23
+ for (const e of this.preInitQueue) {
24
+ this.track(e.name, e.props);
25
+ }
26
+ this.preInitQueue = [];
27
+ this.pageview();
28
+ const origPushState = history.pushState.bind(history);
29
+ const origReplaceState = history.replaceState.bind(history);
30
+ history.pushState = (...args) => {
31
+ origPushState(...args);
32
+ this.pageview();
33
+ };
34
+ history.replaceState = (...args) => {
35
+ origReplaceState(...args);
36
+ this.pageview();
37
+ };
38
+ window.addEventListener("popstate", () => this.pageview());
39
+ this.timer = setInterval(() => this.flush(), 5e3);
40
+ const onHide = () => this.flush(true);
41
+ document.addEventListener("visibilitychange", () => {
42
+ if (document.visibilityState === "hidden") onHide();
43
+ });
44
+ window.addEventListener("pagehide", onHide);
45
+ }
46
+ track(name, properties) {
47
+ if (!this.initialized) {
48
+ this.preInitQueue.push({ name, props: properties });
49
+ return;
50
+ }
51
+ const event = {
52
+ name,
53
+ url: location.href,
54
+ referrer: document.referrer,
55
+ sessionId: this.sessionId,
56
+ anonymousId: this.anonymousId,
57
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
58
+ screenWidth: window.innerWidth,
59
+ screenHeight: window.innerHeight,
60
+ language: navigator.language,
61
+ platform: "web",
62
+ properties
63
+ };
64
+ this.queue.push(event);
65
+ }
66
+ getAnonymousId() {
67
+ return this.anonymousId;
68
+ }
69
+ // ── Private ─────────────────────────────────────────────────────────
70
+ pageview() {
71
+ this.track("pageview");
72
+ }
73
+ flush(useBeacon = false) {
74
+ if (!this.projectId || this.queue.length === 0) return;
75
+ const events = this.queue.splice(0, 100);
76
+ const payload = { p: this.projectId, events };
77
+ const url = `${this.endpoint}/e/batch`;
78
+ const body = JSON.stringify(payload);
79
+ if (useBeacon && typeof navigator.sendBeacon === "function") {
80
+ navigator.sendBeacon(url, body);
81
+ } else {
82
+ fetch(url, {
83
+ method: "POST",
84
+ headers: { "content-type": "application/json" },
85
+ body,
86
+ keepalive: true
87
+ }).catch(() => {
88
+ });
89
+ }
90
+ if (this.queue.length > 0) this.flush(useBeacon);
91
+ }
92
+ newId(prefix) {
93
+ const rand = Math.random().toString(36).slice(2, 10);
94
+ const ts = Date.now().toString(36);
95
+ return `${prefix}_${ts}${rand}`;
96
+ }
97
+ };
98
+ var client = new BrowserClient();
99
+ function init(projectId, opts) {
100
+ client.init(projectId, opts);
101
+ }
102
+ function track(name, properties) {
103
+ client.track(name, properties);
104
+ }
105
+ function getAnonymousId() {
106
+ return client.getAnonymousId();
107
+ }
108
+ export {
109
+ getAnonymousId,
110
+ init,
111
+ track
112
+ };
@@ -0,0 +1,16 @@
1
+ interface AnalyticsProps {
2
+ projectId: string;
3
+ endpoint?: string;
4
+ }
5
+ /**
6
+ * Drop-in React component. Add to your root layout to start tracking.
7
+ * Calls init() in useEffect, renders nothing.
8
+ *
9
+ * ```tsx
10
+ * import { Analytics } from "@clamp-sh/analytics/react"
11
+ * <Analytics projectId="proj_xxx" />
12
+ * ```
13
+ */
14
+ declare function Analytics({ projectId, endpoint }: AnalyticsProps): null;
15
+
16
+ export { Analytics, type AnalyticsProps };
@@ -0,0 +1,16 @@
1
+ interface AnalyticsProps {
2
+ projectId: string;
3
+ endpoint?: string;
4
+ }
5
+ /**
6
+ * Drop-in React component. Add to your root layout to start tracking.
7
+ * Calls init() in useEffect, renders nothing.
8
+ *
9
+ * ```tsx
10
+ * import { Analytics } from "@clamp-sh/analytics/react"
11
+ * <Analytics projectId="proj_xxx" />
12
+ * ```
13
+ */
14
+ declare function Analytics({ projectId, endpoint }: AnalyticsProps): null;
15
+
16
+ export { Analytics, type AnalyticsProps };
package/dist/react.js ADDED
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/react.tsx
22
+ var react_exports = {};
23
+ __export(react_exports, {
24
+ Analytics: () => Analytics
25
+ });
26
+ module.exports = __toCommonJS(react_exports);
27
+ var import_react = require("react");
28
+
29
+ // src/index.ts
30
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
31
+ var BrowserClient = class {
32
+ constructor() {
33
+ this.projectId = null;
34
+ this.endpoint = DEFAULT_ENDPOINT;
35
+ this.sessionId = null;
36
+ this.anonymousId = null;
37
+ this.queue = [];
38
+ this.preInitQueue = [];
39
+ this.timer = null;
40
+ this.initialized = false;
41
+ }
42
+ init(projectId, opts) {
43
+ if (typeof window === "undefined") return;
44
+ this.projectId = projectId;
45
+ if (opts?.endpoint) this.endpoint = opts.endpoint;
46
+ this.initialized = true;
47
+ this.sessionId = sessionStorage.getItem("clamp_sid") ?? this.newId("ses");
48
+ sessionStorage.setItem("clamp_sid", this.sessionId);
49
+ this.anonymousId = localStorage.getItem("clamp_aid") ?? this.newId("anon");
50
+ localStorage.setItem("clamp_aid", this.anonymousId);
51
+ for (const e of this.preInitQueue) {
52
+ this.track(e.name, e.props);
53
+ }
54
+ this.preInitQueue = [];
55
+ this.pageview();
56
+ const origPushState = history.pushState.bind(history);
57
+ const origReplaceState = history.replaceState.bind(history);
58
+ history.pushState = (...args) => {
59
+ origPushState(...args);
60
+ this.pageview();
61
+ };
62
+ history.replaceState = (...args) => {
63
+ origReplaceState(...args);
64
+ this.pageview();
65
+ };
66
+ window.addEventListener("popstate", () => this.pageview());
67
+ this.timer = setInterval(() => this.flush(), 5e3);
68
+ const onHide = () => this.flush(true);
69
+ document.addEventListener("visibilitychange", () => {
70
+ if (document.visibilityState === "hidden") onHide();
71
+ });
72
+ window.addEventListener("pagehide", onHide);
73
+ }
74
+ track(name, properties) {
75
+ if (!this.initialized) {
76
+ this.preInitQueue.push({ name, props: properties });
77
+ return;
78
+ }
79
+ const event = {
80
+ name,
81
+ url: location.href,
82
+ referrer: document.referrer,
83
+ sessionId: this.sessionId,
84
+ anonymousId: this.anonymousId,
85
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
86
+ screenWidth: window.innerWidth,
87
+ screenHeight: window.innerHeight,
88
+ language: navigator.language,
89
+ platform: "web",
90
+ properties
91
+ };
92
+ this.queue.push(event);
93
+ }
94
+ getAnonymousId() {
95
+ return this.anonymousId;
96
+ }
97
+ // ── Private ─────────────────────────────────────────────────────────
98
+ pageview() {
99
+ this.track("pageview");
100
+ }
101
+ flush(useBeacon = false) {
102
+ if (!this.projectId || this.queue.length === 0) return;
103
+ const events = this.queue.splice(0, 100);
104
+ const payload = { p: this.projectId, events };
105
+ const url = `${this.endpoint}/e/batch`;
106
+ const body = JSON.stringify(payload);
107
+ if (useBeacon && typeof navigator.sendBeacon === "function") {
108
+ navigator.sendBeacon(url, body);
109
+ } else {
110
+ fetch(url, {
111
+ method: "POST",
112
+ headers: { "content-type": "application/json" },
113
+ body,
114
+ keepalive: true
115
+ }).catch(() => {
116
+ });
117
+ }
118
+ if (this.queue.length > 0) this.flush(useBeacon);
119
+ }
120
+ newId(prefix) {
121
+ const rand = Math.random().toString(36).slice(2, 10);
122
+ const ts = Date.now().toString(36);
123
+ return `${prefix}_${ts}${rand}`;
124
+ }
125
+ };
126
+ var client = new BrowserClient();
127
+ function init(projectId, opts) {
128
+ client.init(projectId, opts);
129
+ }
130
+
131
+ // src/react.tsx
132
+ function Analytics({ projectId, endpoint }) {
133
+ (0, import_react.useEffect)(() => {
134
+ init(projectId, { endpoint });
135
+ }, [projectId, endpoint]);
136
+ return null;
137
+ }
138
+ // Annotate the CommonJS export names for ESM import in node:
139
+ 0 && (module.exports = {
140
+ Analytics
141
+ });
package/dist/react.mjs ADDED
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ // src/react.tsx
4
+ import { useEffect } from "react";
5
+
6
+ // src/index.ts
7
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
8
+ var BrowserClient = class {
9
+ constructor() {
10
+ this.projectId = null;
11
+ this.endpoint = DEFAULT_ENDPOINT;
12
+ this.sessionId = null;
13
+ this.anonymousId = null;
14
+ this.queue = [];
15
+ this.preInitQueue = [];
16
+ this.timer = null;
17
+ this.initialized = false;
18
+ }
19
+ init(projectId, opts) {
20
+ if (typeof window === "undefined") return;
21
+ this.projectId = projectId;
22
+ if (opts?.endpoint) this.endpoint = opts.endpoint;
23
+ this.initialized = true;
24
+ this.sessionId = sessionStorage.getItem("clamp_sid") ?? this.newId("ses");
25
+ sessionStorage.setItem("clamp_sid", this.sessionId);
26
+ this.anonymousId = localStorage.getItem("clamp_aid") ?? this.newId("anon");
27
+ localStorage.setItem("clamp_aid", this.anonymousId);
28
+ for (const e of this.preInitQueue) {
29
+ this.track(e.name, e.props);
30
+ }
31
+ this.preInitQueue = [];
32
+ this.pageview();
33
+ const origPushState = history.pushState.bind(history);
34
+ const origReplaceState = history.replaceState.bind(history);
35
+ history.pushState = (...args) => {
36
+ origPushState(...args);
37
+ this.pageview();
38
+ };
39
+ history.replaceState = (...args) => {
40
+ origReplaceState(...args);
41
+ this.pageview();
42
+ };
43
+ window.addEventListener("popstate", () => this.pageview());
44
+ this.timer = setInterval(() => this.flush(), 5e3);
45
+ const onHide = () => this.flush(true);
46
+ document.addEventListener("visibilitychange", () => {
47
+ if (document.visibilityState === "hidden") onHide();
48
+ });
49
+ window.addEventListener("pagehide", onHide);
50
+ }
51
+ track(name, properties) {
52
+ if (!this.initialized) {
53
+ this.preInitQueue.push({ name, props: properties });
54
+ return;
55
+ }
56
+ const event = {
57
+ name,
58
+ url: location.href,
59
+ referrer: document.referrer,
60
+ sessionId: this.sessionId,
61
+ anonymousId: this.anonymousId,
62
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
63
+ screenWidth: window.innerWidth,
64
+ screenHeight: window.innerHeight,
65
+ language: navigator.language,
66
+ platform: "web",
67
+ properties
68
+ };
69
+ this.queue.push(event);
70
+ }
71
+ getAnonymousId() {
72
+ return this.anonymousId;
73
+ }
74
+ // ── Private ─────────────────────────────────────────────────────────
75
+ pageview() {
76
+ this.track("pageview");
77
+ }
78
+ flush(useBeacon = false) {
79
+ if (!this.projectId || this.queue.length === 0) return;
80
+ const events = this.queue.splice(0, 100);
81
+ const payload = { p: this.projectId, events };
82
+ const url = `${this.endpoint}/e/batch`;
83
+ const body = JSON.stringify(payload);
84
+ if (useBeacon && typeof navigator.sendBeacon === "function") {
85
+ navigator.sendBeacon(url, body);
86
+ } else {
87
+ fetch(url, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body,
91
+ keepalive: true
92
+ }).catch(() => {
93
+ });
94
+ }
95
+ if (this.queue.length > 0) this.flush(useBeacon);
96
+ }
97
+ newId(prefix) {
98
+ const rand = Math.random().toString(36).slice(2, 10);
99
+ const ts = Date.now().toString(36);
100
+ return `${prefix}_${ts}${rand}`;
101
+ }
102
+ };
103
+ var client = new BrowserClient();
104
+ function init(projectId, opts) {
105
+ client.init(projectId, opts);
106
+ }
107
+
108
+ // src/react.tsx
109
+ function Analytics({ projectId, endpoint }) {
110
+ useEffect(() => {
111
+ init(projectId, { endpoint });
112
+ }, [projectId, endpoint]);
113
+ return null;
114
+ }
115
+ export {
116
+ Analytics
117
+ };
@@ -0,0 +1,24 @@
1
+ import { E as EventMap, A as AnyEvents } from './types-Bc30ZeCx.mjs';
2
+
3
+ /**
4
+ * Initialize the server SDK. Call once at server startup.
5
+ * Requires a project API key (found in project settings).
6
+ */
7
+ declare function init<_E extends EventMap = AnyEvents>(config: {
8
+ projectId: string;
9
+ apiKey: string;
10
+ endpoint?: string;
11
+ }): void;
12
+ /**
13
+ * Track a server-side event. Returns a promise that resolves when the event is sent.
14
+ * Accepts an optional anonymousId to link to a browser visitor.
15
+ */
16
+ declare function track<E extends EventMap = AnyEvents, K extends keyof E & string = string>(name: K, opts?: {
17
+ anonymousId?: string;
18
+ properties?: E[K] extends Record<string, string> ? E[K] : Record<string, string>;
19
+ timestamp?: string;
20
+ }): Promise<{
21
+ ok: boolean;
22
+ }>;
23
+
24
+ export { AnyEvents, EventMap, init, track };
@@ -0,0 +1,24 @@
1
+ import { E as EventMap, A as AnyEvents } from './types-Bc30ZeCx.js';
2
+
3
+ /**
4
+ * Initialize the server SDK. Call once at server startup.
5
+ * Requires a project API key (found in project settings).
6
+ */
7
+ declare function init<_E extends EventMap = AnyEvents>(config: {
8
+ projectId: string;
9
+ apiKey: string;
10
+ endpoint?: string;
11
+ }): void;
12
+ /**
13
+ * Track a server-side event. Returns a promise that resolves when the event is sent.
14
+ * Accepts an optional anonymousId to link to a browser visitor.
15
+ */
16
+ declare function track<E extends EventMap = AnyEvents, K extends keyof E & string = string>(name: K, opts?: {
17
+ anonymousId?: string;
18
+ properties?: E[K] extends Record<string, string> ? E[K] : Record<string, string>;
19
+ timestamp?: string;
20
+ }): Promise<{
21
+ ok: boolean;
22
+ }>;
23
+
24
+ export { AnyEvents, EventMap, init, track };
package/dist/server.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ init: () => init,
24
+ track: () => track
25
+ });
26
+ module.exports = __toCommonJS(server_exports);
27
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
28
+ var ServerClient = class {
29
+ constructor() {
30
+ this.projectId = null;
31
+ this.apiKey = null;
32
+ this.endpoint = DEFAULT_ENDPOINT;
33
+ this.initialized = false;
34
+ }
35
+ init(config) {
36
+ this.projectId = config.projectId;
37
+ this.apiKey = config.apiKey;
38
+ if (config.endpoint) this.endpoint = config.endpoint;
39
+ this.initialized = true;
40
+ }
41
+ async track(name, opts) {
42
+ if (!this.initialized || !this.projectId || !this.apiKey) {
43
+ throw new Error("@clamp-sh/analytics/server: call init() before track()");
44
+ }
45
+ const payload = {
46
+ p: this.projectId,
47
+ name,
48
+ anonymousId: opts?.anonymousId,
49
+ properties: opts?.properties,
50
+ timestamp: opts?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
51
+ };
52
+ const res = await fetch(`${this.endpoint}/e/s`, {
53
+ method: "POST",
54
+ headers: {
55
+ "content-type": "application/json",
56
+ "x-clamp-key": this.apiKey
57
+ },
58
+ body: JSON.stringify(payload)
59
+ });
60
+ if (!res.ok) {
61
+ const body = await res.text().catch(() => "");
62
+ throw new Error(`@clamp-sh/analytics/server: ${res.status} ${body}`);
63
+ }
64
+ return { ok: true };
65
+ }
66
+ };
67
+ var client = new ServerClient();
68
+ function init(config) {
69
+ client.init(config);
70
+ }
71
+ async function track(name, opts) {
72
+ return client.track(name, opts);
73
+ }
74
+ // Annotate the CommonJS export names for ESM import in node:
75
+ 0 && (module.exports = {
76
+ init,
77
+ track
78
+ });
@@ -0,0 +1,52 @@
1
+ // src/server.ts
2
+ var DEFAULT_ENDPOINT = "https://api.clamp.sh";
3
+ var ServerClient = class {
4
+ constructor() {
5
+ this.projectId = null;
6
+ this.apiKey = null;
7
+ this.endpoint = DEFAULT_ENDPOINT;
8
+ this.initialized = false;
9
+ }
10
+ init(config) {
11
+ this.projectId = config.projectId;
12
+ this.apiKey = config.apiKey;
13
+ if (config.endpoint) this.endpoint = config.endpoint;
14
+ this.initialized = true;
15
+ }
16
+ async track(name, opts) {
17
+ if (!this.initialized || !this.projectId || !this.apiKey) {
18
+ throw new Error("@clamp-sh/analytics/server: call init() before track()");
19
+ }
20
+ const payload = {
21
+ p: this.projectId,
22
+ name,
23
+ anonymousId: opts?.anonymousId,
24
+ properties: opts?.properties,
25
+ timestamp: opts?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
26
+ };
27
+ const res = await fetch(`${this.endpoint}/e/s`, {
28
+ method: "POST",
29
+ headers: {
30
+ "content-type": "application/json",
31
+ "x-clamp-key": this.apiKey
32
+ },
33
+ body: JSON.stringify(payload)
34
+ });
35
+ if (!res.ok) {
36
+ const body = await res.text().catch(() => "");
37
+ throw new Error(`@clamp-sh/analytics/server: ${res.status} ${body}`);
38
+ }
39
+ return { ok: true };
40
+ }
41
+ };
42
+ var client = new ServerClient();
43
+ function init(config) {
44
+ client.init(config);
45
+ }
46
+ async function track(name, opts) {
47
+ return client.track(name, opts);
48
+ }
49
+ export {
50
+ init,
51
+ track
52
+ };
@@ -0,0 +1,6 @@
1
+ /** Typed event map for generics. Keys = event names, values = property shapes. */
2
+ type EventMap = Record<string, Record<string, string> | undefined>;
3
+ /** Default event map (no type checking on event names). */
4
+ type AnyEvents = Record<string, Record<string, string> | undefined>;
5
+
6
+ export type { AnyEvents as A, EventMap as E };
@@ -0,0 +1,6 @@
1
+ /** Typed event map for generics. Keys = event names, values = property shapes. */
2
+ type EventMap = Record<string, Record<string, string> | undefined>;
3
+ /** Default event map (no type checking on event names). */
4
+ type AnyEvents = Record<string, Record<string, string> | undefined>;
5
+
6
+ export type { AnyEvents as A, EventMap as E };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@clamp-sh/analytics",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight analytics SDK for Clamp. Auto-pageviews, sessions, batching. Browser, server, and React.",
5
+ "license": "MIT",
6
+ "homepage": "https://clamp.sh",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/clamp-sh/analytics"
10
+ },
11
+ "keywords": ["analytics", "tracking", "pageviews", "sessions", "react", "nextjs"],
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.mjs",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.mjs",
19
+ "require": "./dist/index.js"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/server.d.ts",
23
+ "import": "./dist/server.mjs",
24
+ "require": "./dist/server.js"
25
+ },
26
+ "./react": {
27
+ "types": "./dist/react.d.ts",
28
+ "import": "./dist/react.mjs",
29
+ "require": "./dist/react.js"
30
+ }
31
+ },
32
+ "files": ["dist"],
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "react": { "optional": true }
44
+ },
45
+ "devDependencies": {
46
+ "react": "^19",
47
+ "@types/react": "^19",
48
+ "tsup": "^8",
49
+ "typescript": "^5"
50
+ }
51
+ }