locallytics 0.1.9 → 0.2.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/dist/adapters/drizzle.d.mts +25 -0
- package/dist/adapters/drizzle.d.ts +25 -0
- package/dist/adapters/drizzle.js +38 -0
- package/dist/adapters/drizzle.mjs +7 -0
- package/dist/adapters/prisma.d.mts +25 -0
- package/dist/adapters/prisma.d.ts +25 -0
- package/dist/adapters/prisma.js +38 -0
- package/dist/adapters/prisma.mjs +7 -0
- package/dist/adapters.d.mts +7 -0
- package/dist/adapters.d.ts +7 -0
- package/dist/adapters.js +39 -0
- package/dist/adapters.mjs +8 -0
- package/dist/browser.d.mts +41 -0
- package/dist/browser.d.ts +41 -0
- package/dist/browser.js +79 -0
- package/dist/browser.mjs +10 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -6
- package/dist/index.js +367 -7
- package/dist/index.mjs +336 -0
- package/dist/react.d.mts +48 -0
- package/dist/react.d.ts +48 -0
- package/dist/react.js +132 -0
- package/dist/react.mjs +61 -0
- package/dist/shared/chunk-8tbv1rg5.js +45 -0
- package/package.json +60 -48
- package/README.md +0 -73
- package/dist/client/LocallyticsGrabber.d.ts +0 -30
- package/dist/client/LocallyticsGrabber.d.ts.map +0 -1
- package/dist/client/LocallyticsGrabber.js +0 -71
- package/dist/client/LocallyticsGrabber.js.map +0 -1
- package/dist/client/batcher.d.ts +0 -48
- package/dist/client/batcher.d.ts.map +0 -1
- package/dist/client/batcher.js +0 -134
- package/dist/client/batcher.js.map +0 -1
- package/dist/client/tracker.d.ts +0 -18
- package/dist/client/tracker.d.ts.map +0 -1
- package/dist/client/tracker.js +0 -101
- package/dist/client/tracker.js.map +0 -1
- package/dist/db/factory.d.ts +0 -11
- package/dist/db/factory.d.ts.map +0 -1
- package/dist/db/factory.js +0 -46
- package/dist/db/factory.js.map +0 -1
- package/dist/db/mysql.d.ts +0 -17
- package/dist/db/mysql.d.ts.map +0 -1
- package/dist/db/mysql.js +0 -144
- package/dist/db/mysql.js.map +0 -1
- package/dist/db/postgres.d.ts +0 -16
- package/dist/db/postgres.d.ts.map +0 -1
- package/dist/db/postgres.js +0 -142
- package/dist/db/postgres.js.map +0 -1
- package/dist/db/sqlite.d.ts +0 -17
- package/dist/db/sqlite.d.ts.map +0 -1
- package/dist/db/sqlite.js +0 -130
- package/dist/db/sqlite.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server/handlers.d.ts +0 -10
- package/dist/server/handlers.d.ts.map +0 -1
- package/dist/server/handlers.js +0 -96
- package/dist/server/handlers.js.map +0 -1
- package/dist/server/index.d.ts +0 -42
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -96
- package/dist/server/index.js.map +0 -1
- package/dist/server/queries.d.ts +0 -10
- package/dist/server/queries.d.ts.map +0 -1
- package/dist/server/queries.js +0 -24
- package/dist/server/queries.js.map +0 -1
- package/dist/server/validator.d.ts +0 -32
- package/dist/server/validator.d.ts.map +0 -1
- package/dist/server/validator.js +0 -129
- package/dist/server/validator.js.map +0 -1
- package/dist/types/index.d.ts +0 -135
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -24
- package/dist/types/index.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -16
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -36
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/rate-limit.d.ts +0 -32
- package/dist/utils/rate-limit.d.ts.map +0 -1
- package/dist/utils/rate-limit.js +0 -73
- package/dist/utils/rate-limit.js.map +0 -1
- package/src/db/schema-mysql.sql +0 -17
- package/src/db/schema-sqlite.sql +0 -29
- package/src/db/schema.sql +0 -35
package/dist/index.js
CHANGED
|
@@ -1,7 +1,367 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var exports_src = {};
|
|
32
|
+
__export(exports_src, {
|
|
33
|
+
createLocallytics: () => createLocallytics
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(exports_src);
|
|
36
|
+
function toError(error) {
|
|
37
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
38
|
+
}
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return typeof value === "object" && value !== null;
|
|
41
|
+
}
|
|
42
|
+
function parseMetadata(value) {
|
|
43
|
+
if (value == null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (isRecord(value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(value);
|
|
52
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
function normalizeEventRow(row) {
|
|
60
|
+
return {
|
|
61
|
+
id: row.id,
|
|
62
|
+
type: String(row.type ?? ""),
|
|
63
|
+
timestamp: Number(row.timestamp ?? Date.now()),
|
|
64
|
+
sessionId: row.session_id ?? row.sessionId,
|
|
65
|
+
userId: row.user_id ?? row.userId,
|
|
66
|
+
metadata: parseMetadata(row.metadata)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function stringifyMetadata(metadata) {
|
|
70
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
71
|
+
}
|
|
72
|
+
function isDatabaseAdapter(database) {
|
|
73
|
+
return "insertEvent" in database && typeof database.insertEvent === "function" && "findEvents" in database && typeof database.findEvents === "function";
|
|
74
|
+
}
|
|
75
|
+
function isPostgresLikeDatabase(database) {
|
|
76
|
+
return "query" in database && typeof database.query === "function";
|
|
77
|
+
}
|
|
78
|
+
function isMysqlLikeDatabase(database) {
|
|
79
|
+
return "execute" in database && typeof database.execute === "function";
|
|
80
|
+
}
|
|
81
|
+
function isSqliteLikeDatabase(database) {
|
|
82
|
+
return "prepare" in database && typeof database.prepare === "function";
|
|
83
|
+
}
|
|
84
|
+
function createPostgresAdapter(database) {
|
|
85
|
+
let initialized = false;
|
|
86
|
+
async function ensureInitialized() {
|
|
87
|
+
if (initialized) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await database.query(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
92
|
+
id BIGSERIAL PRIMARY KEY,
|
|
93
|
+
type TEXT NOT NULL,
|
|
94
|
+
timestamp BIGINT NOT NULL,
|
|
95
|
+
session_id TEXT,
|
|
96
|
+
user_id TEXT,
|
|
97
|
+
metadata JSONB
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
initialized = true;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
initialize: ensureInitialized,
|
|
104
|
+
async insertEvent(event) {
|
|
105
|
+
await ensureInitialized();
|
|
106
|
+
const result = await database.query(`
|
|
107
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
108
|
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
109
|
+
RETURNING id, type, timestamp, session_id, user_id, metadata
|
|
110
|
+
`, [
|
|
111
|
+
event.type,
|
|
112
|
+
event.timestamp,
|
|
113
|
+
event.sessionId ?? null,
|
|
114
|
+
event.userId ?? null,
|
|
115
|
+
stringifyMetadata(event.metadata)
|
|
116
|
+
]);
|
|
117
|
+
const first = result.rows[0];
|
|
118
|
+
return first ? normalizeEventRow(first) : { ...event };
|
|
119
|
+
},
|
|
120
|
+
async findEvents() {
|
|
121
|
+
await ensureInitialized();
|
|
122
|
+
const result = await database.query(`
|
|
123
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
124
|
+
FROM locallytics_events
|
|
125
|
+
ORDER BY timestamp DESC
|
|
126
|
+
`);
|
|
127
|
+
return result.rows.map(normalizeEventRow);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function createMysqlAdapter(database) {
|
|
132
|
+
let initialized = false;
|
|
133
|
+
async function ensureInitialized() {
|
|
134
|
+
if (initialized) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await database.execute(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
139
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
140
|
+
type VARCHAR(255) NOT NULL,
|
|
141
|
+
timestamp BIGINT NOT NULL,
|
|
142
|
+
session_id VARCHAR(255),
|
|
143
|
+
user_id VARCHAR(255),
|
|
144
|
+
metadata JSON
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
initialized = true;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
initialize: ensureInitialized,
|
|
151
|
+
async insertEvent(event) {
|
|
152
|
+
await ensureInitialized();
|
|
153
|
+
const [insertResult] = await database.execute(`
|
|
154
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
155
|
+
VALUES (?, ?, ?, ?, ?)
|
|
156
|
+
`, [
|
|
157
|
+
event.type,
|
|
158
|
+
event.timestamp,
|
|
159
|
+
event.sessionId ?? null,
|
|
160
|
+
event.userId ?? null,
|
|
161
|
+
stringifyMetadata(event.metadata)
|
|
162
|
+
]);
|
|
163
|
+
const insertId = Number(insertResult.insertId ?? 0);
|
|
164
|
+
if (!insertId) {
|
|
165
|
+
return { ...event };
|
|
166
|
+
}
|
|
167
|
+
const [rows] = await database.execute(`
|
|
168
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
169
|
+
FROM locallytics_events
|
|
170
|
+
WHERE id = ?
|
|
171
|
+
LIMIT 1
|
|
172
|
+
`, [insertId]);
|
|
173
|
+
const first = rows[0];
|
|
174
|
+
return first ? normalizeEventRow(first) : { ...event, id: insertId };
|
|
175
|
+
},
|
|
176
|
+
async findEvents() {
|
|
177
|
+
await ensureInitialized();
|
|
178
|
+
const [rows] = await database.execute(`
|
|
179
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
180
|
+
FROM locallytics_events
|
|
181
|
+
ORDER BY timestamp DESC
|
|
182
|
+
`);
|
|
183
|
+
return rows.map(normalizeEventRow);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function createSqliteAdapter(database) {
|
|
188
|
+
let initialized = false;
|
|
189
|
+
function ensureInitialized() {
|
|
190
|
+
if (initialized) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
database.prepare(`
|
|
194
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
195
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
196
|
+
type TEXT NOT NULL,
|
|
197
|
+
timestamp INTEGER NOT NULL,
|
|
198
|
+
session_id TEXT,
|
|
199
|
+
user_id TEXT,
|
|
200
|
+
metadata TEXT
|
|
201
|
+
)
|
|
202
|
+
`).run();
|
|
203
|
+
initialized = true;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
initialize: ensureInitialized,
|
|
207
|
+
async insertEvent(event) {
|
|
208
|
+
ensureInitialized();
|
|
209
|
+
const insert = database.prepare(`
|
|
210
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
211
|
+
VALUES (?, ?, ?, ?, ?)
|
|
212
|
+
`);
|
|
213
|
+
const result = insert.run(event.type, event.timestamp, event.sessionId ?? null, event.userId ?? null, stringifyMetadata(event.metadata));
|
|
214
|
+
const insertId = Number(result.lastInsertRowid ?? 0);
|
|
215
|
+
const select = database.prepare(`
|
|
216
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
217
|
+
FROM locallytics_events
|
|
218
|
+
WHERE id = ?
|
|
219
|
+
LIMIT 1
|
|
220
|
+
`);
|
|
221
|
+
const row = select.get ? select.get(insertId) : select.all(insertId)[0];
|
|
222
|
+
return row ? normalizeEventRow(row) : { ...event, id: insertId };
|
|
223
|
+
},
|
|
224
|
+
async findEvents() {
|
|
225
|
+
ensureInitialized();
|
|
226
|
+
const rows = database.prepare(`
|
|
227
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
228
|
+
FROM locallytics_events
|
|
229
|
+
ORDER BY timestamp DESC
|
|
230
|
+
`).all();
|
|
231
|
+
return rows.map(normalizeEventRow);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function createDatabaseAdapter(database) {
|
|
236
|
+
if (isDatabaseAdapter(database)) {
|
|
237
|
+
return database;
|
|
238
|
+
}
|
|
239
|
+
if (isPostgresLikeDatabase(database)) {
|
|
240
|
+
return createPostgresAdapter(database);
|
|
241
|
+
}
|
|
242
|
+
if (isMysqlLikeDatabase(database)) {
|
|
243
|
+
return createMysqlAdapter(database);
|
|
244
|
+
}
|
|
245
|
+
if (isSqliteLikeDatabase(database)) {
|
|
246
|
+
return createSqliteAdapter(database);
|
|
247
|
+
}
|
|
248
|
+
throw new Error("Unsupported database input. Use a pg Pool, mysql2 pool, better-sqlite3 Database, or a DatabaseAdapter.");
|
|
249
|
+
}
|
|
250
|
+
function createSessionId() {
|
|
251
|
+
return `session_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
252
|
+
}
|
|
253
|
+
function createLocallytics(config) {
|
|
254
|
+
const adapter = createDatabaseAdapter(config.database);
|
|
255
|
+
const listeners = new Set;
|
|
256
|
+
const isBrowser = typeof window !== "undefined";
|
|
257
|
+
let userId = config.userId;
|
|
258
|
+
let currentSessionId;
|
|
259
|
+
const queryState = {
|
|
260
|
+
events: [],
|
|
261
|
+
loading: false,
|
|
262
|
+
error: null,
|
|
263
|
+
loaded: false,
|
|
264
|
+
pending: undefined
|
|
265
|
+
};
|
|
266
|
+
function notify() {
|
|
267
|
+
for (const listener of listeners) {
|
|
268
|
+
listener();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function getSessionId() {
|
|
272
|
+
if (!config.sessionTracking) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!currentSessionId) {
|
|
276
|
+
currentSessionId = createSessionId();
|
|
277
|
+
}
|
|
278
|
+
return currentSessionId;
|
|
279
|
+
}
|
|
280
|
+
async function runQuery(force = false) {
|
|
281
|
+
if (!force && queryState.pending) {
|
|
282
|
+
return queryState.pending;
|
|
283
|
+
}
|
|
284
|
+
if (!force && isBrowser && queryState.loaded) {
|
|
285
|
+
return queryState.events;
|
|
286
|
+
}
|
|
287
|
+
queryState.loading = true;
|
|
288
|
+
queryState.error = null;
|
|
289
|
+
notify();
|
|
290
|
+
const pending = (async () => {
|
|
291
|
+
if (adapter.initialize) {
|
|
292
|
+
await adapter.initialize();
|
|
293
|
+
}
|
|
294
|
+
const events = await adapter.findEvents();
|
|
295
|
+
queryState.events = events;
|
|
296
|
+
queryState.loaded = true;
|
|
297
|
+
return events;
|
|
298
|
+
})().catch((error) => {
|
|
299
|
+
const normalized = toError(error);
|
|
300
|
+
queryState.error = normalized;
|
|
301
|
+
throw normalized;
|
|
302
|
+
}).finally(() => {
|
|
303
|
+
queryState.loading = false;
|
|
304
|
+
queryState.pending = undefined;
|
|
305
|
+
notify();
|
|
306
|
+
});
|
|
307
|
+
queryState.pending = pending;
|
|
308
|
+
return pending;
|
|
309
|
+
}
|
|
310
|
+
async function track(type, metadata) {
|
|
311
|
+
if (adapter.initialize) {
|
|
312
|
+
await adapter.initialize();
|
|
313
|
+
}
|
|
314
|
+
const event = {
|
|
315
|
+
type,
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
sessionId: getSessionId(),
|
|
318
|
+
userId,
|
|
319
|
+
metadata
|
|
320
|
+
};
|
|
321
|
+
const saved = await adapter.insertEvent(event);
|
|
322
|
+
if (isBrowser && queryState.loaded) {
|
|
323
|
+
queryState.events = [saved, ...queryState.events];
|
|
324
|
+
notify();
|
|
325
|
+
}
|
|
326
|
+
return saved;
|
|
327
|
+
}
|
|
328
|
+
function createQueryResult(promise) {
|
|
329
|
+
return {
|
|
330
|
+
get events() {
|
|
331
|
+
return queryState.events;
|
|
332
|
+
},
|
|
333
|
+
get loading() {
|
|
334
|
+
return queryState.loading;
|
|
335
|
+
},
|
|
336
|
+
get error() {
|
|
337
|
+
return queryState.error;
|
|
338
|
+
},
|
|
339
|
+
refresh: () => runQuery(true),
|
|
340
|
+
then: promise.then.bind(promise)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
track,
|
|
345
|
+
async trackPageView(route) {
|
|
346
|
+
const resolvedRoute = route ?? (typeof location !== "undefined" ? `${location.pathname}${location.search}` : "/");
|
|
347
|
+
return track("page_view", { route: resolvedRoute });
|
|
348
|
+
},
|
|
349
|
+
query() {
|
|
350
|
+
const promise = runQuery(!isBrowser);
|
|
351
|
+
return createQueryResult(promise);
|
|
352
|
+
},
|
|
353
|
+
setUserId(nextUserId) {
|
|
354
|
+
userId = nextUserId;
|
|
355
|
+
},
|
|
356
|
+
getUserId() {
|
|
357
|
+
return userId;
|
|
358
|
+
},
|
|
359
|
+
getSessionId,
|
|
360
|
+
subscribe(listener) {
|
|
361
|
+
listeners.add(listener);
|
|
362
|
+
return () => {
|
|
363
|
+
listeners.delete(listener);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function toError(error) {
|
|
3
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function parseMetadata(value) {
|
|
9
|
+
if (value == null) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (isRecord(value)) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(value);
|
|
18
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
function normalizeEventRow(row) {
|
|
26
|
+
return {
|
|
27
|
+
id: row.id,
|
|
28
|
+
type: String(row.type ?? ""),
|
|
29
|
+
timestamp: Number(row.timestamp ?? Date.now()),
|
|
30
|
+
sessionId: row.session_id ?? row.sessionId,
|
|
31
|
+
userId: row.user_id ?? row.userId,
|
|
32
|
+
metadata: parseMetadata(row.metadata)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function stringifyMetadata(metadata) {
|
|
36
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
37
|
+
}
|
|
38
|
+
function isDatabaseAdapter(database) {
|
|
39
|
+
return "insertEvent" in database && typeof database.insertEvent === "function" && "findEvents" in database && typeof database.findEvents === "function";
|
|
40
|
+
}
|
|
41
|
+
function isPostgresLikeDatabase(database) {
|
|
42
|
+
return "query" in database && typeof database.query === "function";
|
|
43
|
+
}
|
|
44
|
+
function isMysqlLikeDatabase(database) {
|
|
45
|
+
return "execute" in database && typeof database.execute === "function";
|
|
46
|
+
}
|
|
47
|
+
function isSqliteLikeDatabase(database) {
|
|
48
|
+
return "prepare" in database && typeof database.prepare === "function";
|
|
49
|
+
}
|
|
50
|
+
function createPostgresAdapter(database) {
|
|
51
|
+
let initialized = false;
|
|
52
|
+
async function ensureInitialized() {
|
|
53
|
+
if (initialized) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await database.query(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
58
|
+
id BIGSERIAL PRIMARY KEY,
|
|
59
|
+
type TEXT NOT NULL,
|
|
60
|
+
timestamp BIGINT NOT NULL,
|
|
61
|
+
session_id TEXT,
|
|
62
|
+
user_id TEXT,
|
|
63
|
+
metadata JSONB
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
initialized = true;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
initialize: ensureInitialized,
|
|
70
|
+
async insertEvent(event) {
|
|
71
|
+
await ensureInitialized();
|
|
72
|
+
const result = await database.query(`
|
|
73
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
74
|
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
|
75
|
+
RETURNING id, type, timestamp, session_id, user_id, metadata
|
|
76
|
+
`, [
|
|
77
|
+
event.type,
|
|
78
|
+
event.timestamp,
|
|
79
|
+
event.sessionId ?? null,
|
|
80
|
+
event.userId ?? null,
|
|
81
|
+
stringifyMetadata(event.metadata)
|
|
82
|
+
]);
|
|
83
|
+
const first = result.rows[0];
|
|
84
|
+
return first ? normalizeEventRow(first) : { ...event };
|
|
85
|
+
},
|
|
86
|
+
async findEvents() {
|
|
87
|
+
await ensureInitialized();
|
|
88
|
+
const result = await database.query(`
|
|
89
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
90
|
+
FROM locallytics_events
|
|
91
|
+
ORDER BY timestamp DESC
|
|
92
|
+
`);
|
|
93
|
+
return result.rows.map(normalizeEventRow);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function createMysqlAdapter(database) {
|
|
98
|
+
let initialized = false;
|
|
99
|
+
async function ensureInitialized() {
|
|
100
|
+
if (initialized) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await database.execute(`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
105
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
106
|
+
type VARCHAR(255) NOT NULL,
|
|
107
|
+
timestamp BIGINT NOT NULL,
|
|
108
|
+
session_id VARCHAR(255),
|
|
109
|
+
user_id VARCHAR(255),
|
|
110
|
+
metadata JSON
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
initialized = true;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
initialize: ensureInitialized,
|
|
117
|
+
async insertEvent(event) {
|
|
118
|
+
await ensureInitialized();
|
|
119
|
+
const [insertResult] = await database.execute(`
|
|
120
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?)
|
|
122
|
+
`, [
|
|
123
|
+
event.type,
|
|
124
|
+
event.timestamp,
|
|
125
|
+
event.sessionId ?? null,
|
|
126
|
+
event.userId ?? null,
|
|
127
|
+
stringifyMetadata(event.metadata)
|
|
128
|
+
]);
|
|
129
|
+
const insertId = Number(insertResult.insertId ?? 0);
|
|
130
|
+
if (!insertId) {
|
|
131
|
+
return { ...event };
|
|
132
|
+
}
|
|
133
|
+
const [rows] = await database.execute(`
|
|
134
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
135
|
+
FROM locallytics_events
|
|
136
|
+
WHERE id = ?
|
|
137
|
+
LIMIT 1
|
|
138
|
+
`, [insertId]);
|
|
139
|
+
const first = rows[0];
|
|
140
|
+
return first ? normalizeEventRow(first) : { ...event, id: insertId };
|
|
141
|
+
},
|
|
142
|
+
async findEvents() {
|
|
143
|
+
await ensureInitialized();
|
|
144
|
+
const [rows] = await database.execute(`
|
|
145
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
146
|
+
FROM locallytics_events
|
|
147
|
+
ORDER BY timestamp DESC
|
|
148
|
+
`);
|
|
149
|
+
return rows.map(normalizeEventRow);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function createSqliteAdapter(database) {
|
|
154
|
+
let initialized = false;
|
|
155
|
+
function ensureInitialized() {
|
|
156
|
+
if (initialized) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
database.prepare(`
|
|
160
|
+
CREATE TABLE IF NOT EXISTS locallytics_events (
|
|
161
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
162
|
+
type TEXT NOT NULL,
|
|
163
|
+
timestamp INTEGER NOT NULL,
|
|
164
|
+
session_id TEXT,
|
|
165
|
+
user_id TEXT,
|
|
166
|
+
metadata TEXT
|
|
167
|
+
)
|
|
168
|
+
`).run();
|
|
169
|
+
initialized = true;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
initialize: ensureInitialized,
|
|
173
|
+
async insertEvent(event) {
|
|
174
|
+
ensureInitialized();
|
|
175
|
+
const insert = database.prepare(`
|
|
176
|
+
INSERT INTO locallytics_events (type, timestamp, session_id, user_id, metadata)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?)
|
|
178
|
+
`);
|
|
179
|
+
const result = insert.run(event.type, event.timestamp, event.sessionId ?? null, event.userId ?? null, stringifyMetadata(event.metadata));
|
|
180
|
+
const insertId = Number(result.lastInsertRowid ?? 0);
|
|
181
|
+
const select = database.prepare(`
|
|
182
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
183
|
+
FROM locallytics_events
|
|
184
|
+
WHERE id = ?
|
|
185
|
+
LIMIT 1
|
|
186
|
+
`);
|
|
187
|
+
const row = select.get ? select.get(insertId) : select.all(insertId)[0];
|
|
188
|
+
return row ? normalizeEventRow(row) : { ...event, id: insertId };
|
|
189
|
+
},
|
|
190
|
+
async findEvents() {
|
|
191
|
+
ensureInitialized();
|
|
192
|
+
const rows = database.prepare(`
|
|
193
|
+
SELECT id, type, timestamp, session_id, user_id, metadata
|
|
194
|
+
FROM locallytics_events
|
|
195
|
+
ORDER BY timestamp DESC
|
|
196
|
+
`).all();
|
|
197
|
+
return rows.map(normalizeEventRow);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function createDatabaseAdapter(database) {
|
|
202
|
+
if (isDatabaseAdapter(database)) {
|
|
203
|
+
return database;
|
|
204
|
+
}
|
|
205
|
+
if (isPostgresLikeDatabase(database)) {
|
|
206
|
+
return createPostgresAdapter(database);
|
|
207
|
+
}
|
|
208
|
+
if (isMysqlLikeDatabase(database)) {
|
|
209
|
+
return createMysqlAdapter(database);
|
|
210
|
+
}
|
|
211
|
+
if (isSqliteLikeDatabase(database)) {
|
|
212
|
+
return createSqliteAdapter(database);
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Unsupported database input. Use a pg Pool, mysql2 pool, better-sqlite3 Database, or a DatabaseAdapter.");
|
|
215
|
+
}
|
|
216
|
+
function createSessionId() {
|
|
217
|
+
return `session_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
218
|
+
}
|
|
219
|
+
function createLocallytics(config) {
|
|
220
|
+
const adapter = createDatabaseAdapter(config.database);
|
|
221
|
+
const listeners = new Set;
|
|
222
|
+
const isBrowser = typeof window !== "undefined";
|
|
223
|
+
let userId = config.userId;
|
|
224
|
+
let currentSessionId;
|
|
225
|
+
const queryState = {
|
|
226
|
+
events: [],
|
|
227
|
+
loading: false,
|
|
228
|
+
error: null,
|
|
229
|
+
loaded: false,
|
|
230
|
+
pending: undefined
|
|
231
|
+
};
|
|
232
|
+
function notify() {
|
|
233
|
+
for (const listener of listeners) {
|
|
234
|
+
listener();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function getSessionId() {
|
|
238
|
+
if (!config.sessionTracking) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!currentSessionId) {
|
|
242
|
+
currentSessionId = createSessionId();
|
|
243
|
+
}
|
|
244
|
+
return currentSessionId;
|
|
245
|
+
}
|
|
246
|
+
async function runQuery(force = false) {
|
|
247
|
+
if (!force && queryState.pending) {
|
|
248
|
+
return queryState.pending;
|
|
249
|
+
}
|
|
250
|
+
if (!force && isBrowser && queryState.loaded) {
|
|
251
|
+
return queryState.events;
|
|
252
|
+
}
|
|
253
|
+
queryState.loading = true;
|
|
254
|
+
queryState.error = null;
|
|
255
|
+
notify();
|
|
256
|
+
const pending = (async () => {
|
|
257
|
+
if (adapter.initialize) {
|
|
258
|
+
await adapter.initialize();
|
|
259
|
+
}
|
|
260
|
+
const events = await adapter.findEvents();
|
|
261
|
+
queryState.events = events;
|
|
262
|
+
queryState.loaded = true;
|
|
263
|
+
return events;
|
|
264
|
+
})().catch((error) => {
|
|
265
|
+
const normalized = toError(error);
|
|
266
|
+
queryState.error = normalized;
|
|
267
|
+
throw normalized;
|
|
268
|
+
}).finally(() => {
|
|
269
|
+
queryState.loading = false;
|
|
270
|
+
queryState.pending = undefined;
|
|
271
|
+
notify();
|
|
272
|
+
});
|
|
273
|
+
queryState.pending = pending;
|
|
274
|
+
return pending;
|
|
275
|
+
}
|
|
276
|
+
async function track(type, metadata) {
|
|
277
|
+
if (adapter.initialize) {
|
|
278
|
+
await adapter.initialize();
|
|
279
|
+
}
|
|
280
|
+
const event = {
|
|
281
|
+
type,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
sessionId: getSessionId(),
|
|
284
|
+
userId,
|
|
285
|
+
metadata
|
|
286
|
+
};
|
|
287
|
+
const saved = await adapter.insertEvent(event);
|
|
288
|
+
if (isBrowser && queryState.loaded) {
|
|
289
|
+
queryState.events = [saved, ...queryState.events];
|
|
290
|
+
notify();
|
|
291
|
+
}
|
|
292
|
+
return saved;
|
|
293
|
+
}
|
|
294
|
+
function createQueryResult(promise) {
|
|
295
|
+
return {
|
|
296
|
+
get events() {
|
|
297
|
+
return queryState.events;
|
|
298
|
+
},
|
|
299
|
+
get loading() {
|
|
300
|
+
return queryState.loading;
|
|
301
|
+
},
|
|
302
|
+
get error() {
|
|
303
|
+
return queryState.error;
|
|
304
|
+
},
|
|
305
|
+
refresh: () => runQuery(true),
|
|
306
|
+
then: promise.then.bind(promise)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
track,
|
|
311
|
+
async trackPageView(route) {
|
|
312
|
+
const resolvedRoute = route ?? (typeof location !== "undefined" ? `${location.pathname}${location.search}` : "/");
|
|
313
|
+
return track("page_view", { route: resolvedRoute });
|
|
314
|
+
},
|
|
315
|
+
query() {
|
|
316
|
+
const promise = runQuery(!isBrowser);
|
|
317
|
+
return createQueryResult(promise);
|
|
318
|
+
},
|
|
319
|
+
setUserId(nextUserId) {
|
|
320
|
+
userId = nextUserId;
|
|
321
|
+
},
|
|
322
|
+
getUserId() {
|
|
323
|
+
return userId;
|
|
324
|
+
},
|
|
325
|
+
getSessionId,
|
|
326
|
+
subscribe(listener) {
|
|
327
|
+
listeners.add(listener);
|
|
328
|
+
return () => {
|
|
329
|
+
listeners.delete(listener);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export {
|
|
335
|
+
createLocallytics
|
|
336
|
+
};
|