@nextlytics/core 0.2.0-canary.40 → 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/anonymous-user.cjs +118 -0
- package/dist/anonymous-user.d.mts +22 -0
- package/dist/anonymous-user.js +2 -26
- package/dist/backends/clickhouse.cjs +110 -0
- package/dist/backends/clickhouse.d.mts +58 -0
- package/dist/backends/clickhouse.js +14 -32
- package/dist/backends/ga.cjs +207 -0
- package/dist/backends/ga.d.mts +21 -0
- package/dist/backends/ga.js +2 -26
- package/dist/backends/gtm.cjs +155 -0
- package/dist/backends/gtm.d.mts +11 -0
- package/dist/backends/gtm.js +2 -26
- package/dist/backends/lib/db.cjs +150 -0
- package/dist/backends/lib/db.d.mts +121 -0
- package/dist/backends/lib/db.js +2 -33
- package/dist/backends/logging.cjs +45 -0
- package/dist/backends/logging.d.mts +7 -0
- package/dist/backends/logging.js +2 -26
- package/dist/backends/neon.cjs +84 -0
- package/dist/backends/neon.d.mts +11 -0
- package/dist/backends/neon.js +18 -36
- package/dist/backends/postgrest.cjs +98 -0
- package/dist/backends/postgrest.d.mts +46 -0
- package/dist/backends/postgrest.js +8 -33
- package/dist/backends/posthog.cjs +120 -0
- package/dist/backends/posthog.d.mts +13 -0
- package/dist/backends/posthog.js +2 -26
- package/dist/backends/segment.cjs +112 -0
- package/dist/backends/segment.d.mts +43 -0
- package/dist/backends/segment.js +2 -26
- package/dist/client.cjs +171 -0
- package/dist/client.d.mts +29 -0
- package/dist/client.js +16 -41
- package/dist/config-helpers.cjs +71 -0
- package/dist/config-helpers.d.mts +16 -0
- package/dist/config-helpers.js +2 -28
- package/dist/handlers.cjs +123 -0
- package/dist/handlers.d.mts +9 -0
- package/dist/handlers.js +11 -35
- package/dist/headers.cjs +41 -0
- package/dist/headers.d.mts +3 -0
- package/dist/headers.js +2 -26
- package/dist/index.cjs +41 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.js +6 -35
- package/dist/middleware.cjs +204 -0
- package/dist/middleware.d.mts +10 -0
- package/dist/middleware.js +26 -47
- package/dist/pages-router.cjs +45 -0
- package/dist/pages-router.d.mts +45 -0
- package/dist/pages-router.js +4 -28
- package/dist/plugins/vercel-geo.cjs +60 -0
- package/dist/plugins/vercel-geo.d.mts +25 -0
- package/dist/plugins/vercel-geo.js +2 -26
- package/dist/server-component-context.cjs +95 -0
- package/dist/server-component-context.d.mts +30 -0
- package/dist/server-component-context.js +3 -29
- package/dist/server.cjs +236 -0
- package/dist/server.d.mts +13 -0
- package/dist/server.js +33 -56
- package/dist/template.cjs +108 -0
- package/dist/template.d.mts +27 -0
- package/dist/template.js +2 -27
- package/dist/types.cjs +16 -0
- package/dist/types.d.mts +216 -0
- package/dist/types.js +0 -16
- package/dist/uitils.cjs +94 -0
- package/dist/uitils.d.mts +22 -0
- package/dist/uitils.js +4 -30
- package/package.json +99 -26
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
var anonymous_user_exports = {};
|
|
20
|
+
__export(anonymous_user_exports, {
|
|
21
|
+
resolveAnonymousUser: () => resolveAnonymousUser
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(anonymous_user_exports);
|
|
24
|
+
const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
25
|
+
const ANON_ID_LENGTH = 10;
|
|
26
|
+
const DEFAULTS = {
|
|
27
|
+
gdprMode: true,
|
|
28
|
+
useCookies: false,
|
|
29
|
+
dailySalt: true,
|
|
30
|
+
cookieName: "__nextlytics_anon",
|
|
31
|
+
cookieMaxAge: 60 * 60 * 24 * 365 * 2
|
|
32
|
+
// 2 years
|
|
33
|
+
};
|
|
34
|
+
function isSecureRequest(headers) {
|
|
35
|
+
const proto = headers.get("x-forwarded-proto");
|
|
36
|
+
if (proto) return proto === "https";
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function getDailySalt() {
|
|
40
|
+
const now = /* @__PURE__ */ new Date();
|
|
41
|
+
return `${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}`;
|
|
42
|
+
}
|
|
43
|
+
function bytesToBase62(bytes, length) {
|
|
44
|
+
let result = "";
|
|
45
|
+
for (let i = 0; i < length; i++) {
|
|
46
|
+
const idx = (bytes[i * 2] * 256 + bytes[i * 2 + 1]) % 62;
|
|
47
|
+
result += BASE62_CHARS[idx];
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
async function hashToShortId(input) {
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
const data = encoder.encode(input);
|
|
54
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
55
|
+
return bytesToBase62(new Uint8Array(hashBuffer), ANON_ID_LENGTH);
|
|
56
|
+
}
|
|
57
|
+
async function generateGdprAnonId(serverContext, useDailySalt) {
|
|
58
|
+
const ip = serverContext.ip || "unknown";
|
|
59
|
+
const userAgent = serverContext.requestHeaders["user-agent"] || "unknown";
|
|
60
|
+
const host = serverContext.host || "unknown";
|
|
61
|
+
const parts = useDailySalt ? [getDailySalt(), ip, userAgent, host] : [ip, userAgent, host];
|
|
62
|
+
return hashToShortId(parts.join("|"));
|
|
63
|
+
}
|
|
64
|
+
function generateRandomAnonId() {
|
|
65
|
+
const bytes = new Uint8Array(ANON_ID_LENGTH * 2);
|
|
66
|
+
crypto.getRandomValues(bytes);
|
|
67
|
+
return bytesToBase62(bytes, ANON_ID_LENGTH);
|
|
68
|
+
}
|
|
69
|
+
async function resolveAnonymousUser(params) {
|
|
70
|
+
const { ctx, serverContext, config, response } = params;
|
|
71
|
+
const { headers, cookies } = ctx;
|
|
72
|
+
const gdprMode = config.anonymousUsers?.gdprMode ?? DEFAULTS.gdprMode;
|
|
73
|
+
const useCookies = config.anonymousUsers?.useCookies ?? DEFAULTS.useCookies;
|
|
74
|
+
const dailySalt = config.anonymousUsers?.dailySalt ?? DEFAULTS.dailySalt;
|
|
75
|
+
const cookieName = config.anonymousUsers?.cookieName ?? DEFAULTS.cookieName;
|
|
76
|
+
const cookieMaxAge = config.anonymousUsers?.cookieMaxAge ?? DEFAULTS.cookieMaxAge;
|
|
77
|
+
let anonId;
|
|
78
|
+
let shouldSetCookie = false;
|
|
79
|
+
if (useCookies) {
|
|
80
|
+
const existingCookie = cookies.get(cookieName);
|
|
81
|
+
if (existingCookie?.value) {
|
|
82
|
+
anonId = existingCookie.value;
|
|
83
|
+
} else {
|
|
84
|
+
anonId = gdprMode ? await generateGdprAnonId(serverContext, dailySalt) : generateRandomAnonId();
|
|
85
|
+
shouldSetCookie = true;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (gdprMode) {
|
|
89
|
+
anonId = await generateGdprAnonId(serverContext, dailySalt);
|
|
90
|
+
} else {
|
|
91
|
+
anonId = generateRandomAnonId();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (config.callbacks?.getAnonymousUserId) {
|
|
95
|
+
try {
|
|
96
|
+
const overrideResult = await config.callbacks.getAnonymousUserId({
|
|
97
|
+
ctx,
|
|
98
|
+
originalAnonymousUserId: anonId
|
|
99
|
+
});
|
|
100
|
+
anonId = overrideResult.anonId;
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (shouldSetCookie && response) {
|
|
105
|
+
response.cookies.set(cookieName, anonId, {
|
|
106
|
+
maxAge: cookieMaxAge,
|
|
107
|
+
httpOnly: true,
|
|
108
|
+
secure: isSecureRequest(headers),
|
|
109
|
+
sameSite: "lax",
|
|
110
|
+
path: "/"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { anonId };
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
resolveAnonymousUser
|
|
118
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { RequestContext, ServerEventContext, NextlyticsConfig, AnonymousUserResult } from './types.mjs';
|
|
3
|
+
import 'next/dist/server/web/spec-extension/cookies';
|
|
4
|
+
|
|
5
|
+
type ResolveAnonymousUserParams = {
|
|
6
|
+
ctx: RequestContext;
|
|
7
|
+
serverContext: ServerEventContext;
|
|
8
|
+
config: NextlyticsConfig;
|
|
9
|
+
/** Optional response for setting cookies. If not provided, cookies won't be set. */
|
|
10
|
+
response?: NextResponse | null;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve anonymous user ID based on config.
|
|
14
|
+
*
|
|
15
|
+
* Modes:
|
|
16
|
+
* - gdprMode=true (default): Hash-based ID (Fathom-like)
|
|
17
|
+
* - gdprMode=false + useCookies=true: Persistent cookie-based ID
|
|
18
|
+
* - gdprMode=false + useCookies=false: Random ID per request
|
|
19
|
+
*/
|
|
20
|
+
declare function resolveAnonymousUser(params: ResolveAnonymousUserParams): Promise<AnonymousUserResult>;
|
|
21
|
+
|
|
22
|
+
export { type ResolveAnonymousUserParams, resolveAnonymousUser };
|
package/dist/anonymous-user.js
CHANGED
|
@@ -1,26 +1,3 @@
|
|
|
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
|
-
var anonymous_user_exports = {};
|
|
20
|
-
__export(anonymous_user_exports, {
|
|
21
|
-
resolveAnonymousUser: () => resolveAnonymousUser
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(anonymous_user_exports);
|
|
24
1
|
const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
25
2
|
const ANON_ID_LENGTH = 10;
|
|
26
3
|
const DEFAULTS = {
|
|
@@ -112,7 +89,6 @@ async function resolveAnonymousUser(params) {
|
|
|
112
89
|
}
|
|
113
90
|
return { anonId };
|
|
114
91
|
}
|
|
115
|
-
|
|
116
|
-
0 && (module.exports = {
|
|
92
|
+
export {
|
|
117
93
|
resolveAnonymousUser
|
|
118
|
-
}
|
|
94
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
var clickhouse_exports = {};
|
|
20
|
+
__export(clickhouse_exports, {
|
|
21
|
+
clickhouseBackend: () => clickhouseBackend
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(clickhouse_exports);
|
|
24
|
+
var import_db = require("./lib/db");
|
|
25
|
+
function clickhouseBackend(config) {
|
|
26
|
+
const baseUrl = config.url.replace(/\/$/, "");
|
|
27
|
+
const database = config.database ?? "default";
|
|
28
|
+
const table = config.tableName ?? "analytics";
|
|
29
|
+
const asyncInsert = config.asyncInsert ?? true;
|
|
30
|
+
const acceptUpdates = config.acceptUpdates ?? false;
|
|
31
|
+
const updateLookbackMinutes = config.updateLookbackMinutes ?? 60;
|
|
32
|
+
const authHeader = "Basic " + btoa((config.username ?? "default") + ":" + (config.password ?? ""));
|
|
33
|
+
function printCreateTableStatement() {
|
|
34
|
+
console.error(`[Nextlytics ClickHouse] Table "${database}.${table}" does not exist. Run:
|
|
35
|
+
`);
|
|
36
|
+
console.error((0, import_db.generateChCreateTableSQL)(database, table));
|
|
37
|
+
}
|
|
38
|
+
async function query(sql) {
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
database,
|
|
41
|
+
query: sql
|
|
42
|
+
});
|
|
43
|
+
const res = await fetch(`${baseUrl}/?${params}`, {
|
|
44
|
+
method: "GET",
|
|
45
|
+
headers: { Authorization: authHeader }
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(`ClickHouse query error ${res.status}: ${await res.text()}`);
|
|
49
|
+
}
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
if (!text.trim()) return [];
|
|
52
|
+
return text.trim().split("\n").map((line) => JSON.parse(line));
|
|
53
|
+
}
|
|
54
|
+
async function insert(row) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
database,
|
|
57
|
+
query: `INSERT INTO ${table} FORMAT JSONEachRow`,
|
|
58
|
+
date_time_input_format: "best_effort"
|
|
59
|
+
});
|
|
60
|
+
if (asyncInsert) {
|
|
61
|
+
params.set("async_insert", "1");
|
|
62
|
+
params.set("wait_for_async_insert", "0");
|
|
63
|
+
}
|
|
64
|
+
const res = await fetch(`${baseUrl}/?${params}`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Authorization: authHeader
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(row)
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
if ((0, import_db.isChTableNotFoundError)(text)) {
|
|
75
|
+
printCreateTableStatement();
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`ClickHouse error ${res.status}: ${text}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: "clickhouse",
|
|
82
|
+
supportsUpdates: acceptUpdates,
|
|
83
|
+
async onEvent(event) {
|
|
84
|
+
const row = (0, import_db.eventToJsonRow)(event);
|
|
85
|
+
await insert(row);
|
|
86
|
+
},
|
|
87
|
+
async updateEvent(eventId, patch) {
|
|
88
|
+
if (!acceptUpdates || !patch.clientContext) return;
|
|
89
|
+
const cols = import_db.tableColumns.map((c) => c.name).join(", ");
|
|
90
|
+
const rows = await query(
|
|
91
|
+
`SELECT ${cols} FROM ${table} WHERE event_id = '${eventId}' AND timestamp > now() - INTERVAL ${updateLookbackMinutes} MINUTE FORMAT JSONEachRow`
|
|
92
|
+
);
|
|
93
|
+
if (rows.length === 0) return;
|
|
94
|
+
const existing = rows[0];
|
|
95
|
+
const clientCtx = (0, import_db.extractClientContext)(patch.clientContext);
|
|
96
|
+
const updated = {
|
|
97
|
+
...existing,
|
|
98
|
+
referer: clientCtx.referer ?? existing.referer,
|
|
99
|
+
user_agent: clientCtx.user_agent ?? existing.user_agent,
|
|
100
|
+
locale: clientCtx.locale ?? existing.locale,
|
|
101
|
+
client_context: clientCtx.rest ?? existing.client_context
|
|
102
|
+
};
|
|
103
|
+
await insert(updated);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
108
|
+
0 && (module.exports = {
|
|
109
|
+
clickhouseBackend
|
|
110
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextlyticsBackend } from '../types.mjs';
|
|
2
|
+
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
|
+
import 'next/server';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ClickHouse backend for Nextlytics
|
|
7
|
+
*
|
|
8
|
+
* Sends events to ClickHouse via HTTP API with async inserts.
|
|
9
|
+
*
|
|
10
|
+
* ## ClickHouse Cloud Usage
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { clickhouseBackend } from "@nextlytics/core/backends/clickhouse"
|
|
14
|
+
*
|
|
15
|
+
* const analytics = Nextlytics({
|
|
16
|
+
* backends: [
|
|
17
|
+
* clickhouseBackend({
|
|
18
|
+
* url: "https://xxx.clickhouse.cloud:8443",
|
|
19
|
+
* username: "default",
|
|
20
|
+
* password: process.env.CLICKHOUSE_PASSWORD!,
|
|
21
|
+
* })
|
|
22
|
+
* ]
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## Self-hosted ClickHouse
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* clickhouseBackend({
|
|
30
|
+
* url: "http://localhost:8123",
|
|
31
|
+
* username: "default",
|
|
32
|
+
* password: "",
|
|
33
|
+
* database: "analytics",
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
type ClickHouseBackendConfig = {
|
|
39
|
+
/** ClickHouse HTTP API URL */
|
|
40
|
+
url: string;
|
|
41
|
+
/** Username (default: "default") */
|
|
42
|
+
username?: string;
|
|
43
|
+
/** Password */
|
|
44
|
+
password?: string;
|
|
45
|
+
/** Database name (default: "default") */
|
|
46
|
+
database?: string;
|
|
47
|
+
/** Table name (default: "analytics") */
|
|
48
|
+
tableName?: string;
|
|
49
|
+
/** Enable async inserts (default: true) */
|
|
50
|
+
asyncInsert?: boolean;
|
|
51
|
+
/** Enable updates via select+insert (uses ReplacingMergeTree deduplication) */
|
|
52
|
+
acceptUpdates?: boolean;
|
|
53
|
+
/** Lookback window in minutes for update SELECT queries (default: 60) */
|
|
54
|
+
updateLookbackMinutes?: number;
|
|
55
|
+
};
|
|
56
|
+
declare function clickhouseBackend(config: ClickHouseBackendConfig): NextlyticsBackend;
|
|
57
|
+
|
|
58
|
+
export { type ClickHouseBackendConfig, clickhouseBackend };
|
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
var clickhouse_exports = {};
|
|
20
|
-
__export(clickhouse_exports, {
|
|
21
|
-
clickhouseBackend: () => clickhouseBackend
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(clickhouse_exports);
|
|
24
|
-
var import_db = require("./lib/db");
|
|
1
|
+
import {
|
|
2
|
+
tableColumns,
|
|
3
|
+
eventToJsonRow,
|
|
4
|
+
generateChCreateTableSQL,
|
|
5
|
+
isChTableNotFoundError,
|
|
6
|
+
extractClientContext
|
|
7
|
+
} from "./lib/db";
|
|
25
8
|
function clickhouseBackend(config) {
|
|
26
9
|
const baseUrl = config.url.replace(/\/$/, "");
|
|
27
10
|
const database = config.database ?? "default";
|
|
@@ -33,7 +16,7 @@ function clickhouseBackend(config) {
|
|
|
33
16
|
function printCreateTableStatement() {
|
|
34
17
|
console.error(`[Nextlytics ClickHouse] Table "${database}.${table}" does not exist. Run:
|
|
35
18
|
`);
|
|
36
|
-
console.error(
|
|
19
|
+
console.error(generateChCreateTableSQL(database, table));
|
|
37
20
|
}
|
|
38
21
|
async function query(sql) {
|
|
39
22
|
const params = new URLSearchParams({
|
|
@@ -71,7 +54,7 @@ function clickhouseBackend(config) {
|
|
|
71
54
|
});
|
|
72
55
|
if (!res.ok) {
|
|
73
56
|
const text = await res.text();
|
|
74
|
-
if (
|
|
57
|
+
if (isChTableNotFoundError(text)) {
|
|
75
58
|
printCreateTableStatement();
|
|
76
59
|
}
|
|
77
60
|
throw new Error(`ClickHouse error ${res.status}: ${text}`);
|
|
@@ -81,18 +64,18 @@ function clickhouseBackend(config) {
|
|
|
81
64
|
name: "clickhouse",
|
|
82
65
|
supportsUpdates: acceptUpdates,
|
|
83
66
|
async onEvent(event) {
|
|
84
|
-
const row =
|
|
67
|
+
const row = eventToJsonRow(event);
|
|
85
68
|
await insert(row);
|
|
86
69
|
},
|
|
87
70
|
async updateEvent(eventId, patch) {
|
|
88
71
|
if (!acceptUpdates || !patch.clientContext) return;
|
|
89
|
-
const cols =
|
|
72
|
+
const cols = tableColumns.map((c) => c.name).join(", ");
|
|
90
73
|
const rows = await query(
|
|
91
74
|
`SELECT ${cols} FROM ${table} WHERE event_id = '${eventId}' AND timestamp > now() - INTERVAL ${updateLookbackMinutes} MINUTE FORMAT JSONEachRow`
|
|
92
75
|
);
|
|
93
76
|
if (rows.length === 0) return;
|
|
94
77
|
const existing = rows[0];
|
|
95
|
-
const clientCtx =
|
|
78
|
+
const clientCtx = extractClientContext(patch.clientContext);
|
|
96
79
|
const updated = {
|
|
97
80
|
...existing,
|
|
98
81
|
referer: clientCtx.referer ?? existing.referer,
|
|
@@ -104,7 +87,6 @@ function clickhouseBackend(config) {
|
|
|
104
87
|
}
|
|
105
88
|
};
|
|
106
89
|
}
|
|
107
|
-
|
|
108
|
-
0 && (module.exports = {
|
|
90
|
+
export {
|
|
109
91
|
clickhouseBackend
|
|
110
|
-
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
var ga_exports = {};
|
|
20
|
+
__export(ga_exports, {
|
|
21
|
+
googleAnalyticsBackend: () => googleAnalyticsBackend
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(ga_exports);
|
|
24
|
+
const GA_TEMPLATE_ID = "ga-gtag";
|
|
25
|
+
function parseGaCookie(cookieValue) {
|
|
26
|
+
const match = cookieValue.match(/^GA\d+\.\d+\.(.+)$/);
|
|
27
|
+
return match ? match[1] : null;
|
|
28
|
+
}
|
|
29
|
+
function getClientId(event, gaCookieClientId, source) {
|
|
30
|
+
if (source === "gaCookie" && gaCookieClientId) {
|
|
31
|
+
return gaCookieClientId;
|
|
32
|
+
}
|
|
33
|
+
if (!event.anonymousUserId) {
|
|
34
|
+
throw new Error("anonymousUserId is required for GA backend (no _ga cookie available)");
|
|
35
|
+
}
|
|
36
|
+
return event.anonymousUserId;
|
|
37
|
+
}
|
|
38
|
+
function toGA4EventName(type) {
|
|
39
|
+
if (type === "pageView") return "page_view";
|
|
40
|
+
if (type === "apiCall") return "api_call";
|
|
41
|
+
return type.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
42
|
+
}
|
|
43
|
+
function buildEventParams(event) {
|
|
44
|
+
const params = {
|
|
45
|
+
// Required for engagement metrics
|
|
46
|
+
engagement_time_msec: 1
|
|
47
|
+
};
|
|
48
|
+
const { serverContext } = event;
|
|
49
|
+
if (serverContext) {
|
|
50
|
+
params.page_location = `https://${serverContext.host}${serverContext.path}`;
|
|
51
|
+
}
|
|
52
|
+
const { clientContext } = event;
|
|
53
|
+
if (clientContext) {
|
|
54
|
+
if (clientContext.referer) {
|
|
55
|
+
params.page_referrer = clientContext.referer;
|
|
56
|
+
}
|
|
57
|
+
if (clientContext.locale) {
|
|
58
|
+
params.language = clientContext.locale;
|
|
59
|
+
}
|
|
60
|
+
if (clientContext.screen?.width && clientContext.screen?.height) {
|
|
61
|
+
params.screen_resolution = `${clientContext.screen.width}x${clientContext.screen.height}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (event.properties) {
|
|
65
|
+
Object.assign(params, event.properties);
|
|
66
|
+
}
|
|
67
|
+
return params;
|
|
68
|
+
}
|
|
69
|
+
function getUserAgent(event) {
|
|
70
|
+
return event.clientContext?.userAgent ?? event.serverContext?.requestHeaders?.["user-agent"];
|
|
71
|
+
}
|
|
72
|
+
function getClientIp(event) {
|
|
73
|
+
return event.serverContext?.ip;
|
|
74
|
+
}
|
|
75
|
+
async function sendToMeasurementProtocol(opts) {
|
|
76
|
+
const endpoint = opts.debugMode ? "https://www.google-analytics.com/debug/mp/collect" : "https://www.google-analytics.com/mp/collect";
|
|
77
|
+
const url = `${endpoint}?measurement_id=${opts.measurementId}&api_secret=${opts.apiSecret}`;
|
|
78
|
+
const payload = {
|
|
79
|
+
client_id: opts.clientId,
|
|
80
|
+
events: [
|
|
81
|
+
{
|
|
82
|
+
name: opts.eventName,
|
|
83
|
+
params: opts.eventParams
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
if (opts.userId) {
|
|
88
|
+
payload.user_id = opts.userId;
|
|
89
|
+
}
|
|
90
|
+
if (opts.userProperties && Object.keys(opts.userProperties).length > 0) {
|
|
91
|
+
payload.user_properties = Object.fromEntries(
|
|
92
|
+
Object.entries(opts.userProperties).map(([k, v]) => [k, { value: v }])
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const headers = {};
|
|
96
|
+
if (opts.userAgent) {
|
|
97
|
+
headers["User-Agent"] = opts.userAgent;
|
|
98
|
+
}
|
|
99
|
+
if (opts.clientIp) {
|
|
100
|
+
headers["X-Forwarded-For"] = opts.clientIp;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: JSON.stringify(payload),
|
|
106
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const body = await res.text().catch(() => "");
|
|
110
|
+
console.warn(`[GA] Measurement Protocol error: ${res.status} ${res.statusText}`, body);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn("[GA] Measurement Protocol request failed:", err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function googleAnalyticsBackend(opts) {
|
|
117
|
+
const { measurementId, debugMode, apiSecret, clientIdSource = "gaCookie" } = opts;
|
|
118
|
+
return (ctx) => {
|
|
119
|
+
const gaCookie = ctx.cookies.get("_ga");
|
|
120
|
+
const gaCookieClientId = gaCookie ? parseGaCookie(gaCookie.value) : null;
|
|
121
|
+
return {
|
|
122
|
+
name: "google-analytics",
|
|
123
|
+
returnsClientActions: true,
|
|
124
|
+
supportsUpdates: false,
|
|
125
|
+
getClientSideTemplates() {
|
|
126
|
+
return {
|
|
127
|
+
[GA_TEMPLATE_ID]: {
|
|
128
|
+
items: [
|
|
129
|
+
{
|
|
130
|
+
async: "true",
|
|
131
|
+
src: "https://www.googletagmanager.com/gtag/js?id={{measurementId}}",
|
|
132
|
+
singleton: true
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
body: [
|
|
136
|
+
"window.dataLayer = window.dataLayer || [];",
|
|
137
|
+
"function gtag(){dataLayer.push(arguments);}",
|
|
138
|
+
"gtag('js', new Date());",
|
|
139
|
+
"gtag('config', '{{measurementId}}', {{json(config)}});",
|
|
140
|
+
"gtag('event', 'page_view');"
|
|
141
|
+
].join("\n")
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
async onEvent(event) {
|
|
148
|
+
const clientId = getClientId(event, gaCookieClientId, clientIdSource);
|
|
149
|
+
const userId = event.userContext?.userId;
|
|
150
|
+
const {
|
|
151
|
+
email: _email,
|
|
152
|
+
name: _name,
|
|
153
|
+
phone: _phone,
|
|
154
|
+
...customTraits
|
|
155
|
+
} = event.userContext?.traits ?? {};
|
|
156
|
+
const userProperties = Object.keys(customTraits).length > 0 ? customTraits : void 0;
|
|
157
|
+
if (event.type === "pageView") {
|
|
158
|
+
const config = {
|
|
159
|
+
send_page_view: false,
|
|
160
|
+
client_id: clientId
|
|
161
|
+
};
|
|
162
|
+
if (debugMode) {
|
|
163
|
+
config.debug_mode = true;
|
|
164
|
+
}
|
|
165
|
+
if (userId) {
|
|
166
|
+
config.user_id = userId;
|
|
167
|
+
}
|
|
168
|
+
if (userProperties) {
|
|
169
|
+
config.user_properties = userProperties;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
type: "script-template",
|
|
175
|
+
templateId: GA_TEMPLATE_ID,
|
|
176
|
+
params: {
|
|
177
|
+
measurementId,
|
|
178
|
+
config
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (apiSecret) {
|
|
185
|
+
await sendToMeasurementProtocol({
|
|
186
|
+
measurementId,
|
|
187
|
+
apiSecret,
|
|
188
|
+
clientId,
|
|
189
|
+
userId,
|
|
190
|
+
userProperties,
|
|
191
|
+
eventName: toGA4EventName(event.type),
|
|
192
|
+
eventParams: buildEventParams(event),
|
|
193
|
+
userAgent: getUserAgent(event),
|
|
194
|
+
clientIp: getClientIp(event),
|
|
195
|
+
debugMode
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
updateEvent() {
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
205
|
+
0 && (module.exports = {
|
|
206
|
+
googleAnalyticsBackend
|
|
207
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextlyticsBackendFactory } from '../types.mjs';
|
|
2
|
+
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
|
+
import 'next/server';
|
|
4
|
+
|
|
5
|
+
type GoogleAnalyticsBackendOptions = {
|
|
6
|
+
/** GA4 Measurement ID (e.g. "G-XXXXXXXXXX") */
|
|
7
|
+
measurementId: string;
|
|
8
|
+
/** Enable GA4 debug mode (shows events in DebugView) */
|
|
9
|
+
debugMode?: boolean;
|
|
10
|
+
/** API secret for Measurement Protocol (GA4 Admin → Data Streams → MP secrets) */
|
|
11
|
+
apiSecret?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Source for client_id.
|
|
14
|
+
* - "gaCookie" (default): Use _ga cookie set by gtag.js, fall back to anonymousUserId
|
|
15
|
+
* - "anonymousUserId": Always use Nextlytics anonymousUserId
|
|
16
|
+
*/
|
|
17
|
+
clientIdSource?: "gaCookie" | "anonymousUserId";
|
|
18
|
+
};
|
|
19
|
+
declare function googleAnalyticsBackend(opts: GoogleAnalyticsBackendOptions): NextlyticsBackendFactory;
|
|
20
|
+
|
|
21
|
+
export { type GoogleAnalyticsBackendOptions, googleAnalyticsBackend };
|