@better-auth/telemetry 1.3.28
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/.turbo/turbo-build.log +13 -0
- package/LICENSE.md +17 -0
- package/build.config.ts +10 -0
- package/dist/index.cjs +564 -0
- package/dist/index.d.cts +262 -0
- package/dist/index.d.mts +262 -0
- package/dist/index.d.ts +262 -0
- package/dist/index.mjs +561 -0
- package/package.json +41 -0
- package/src/detectors/detect-auth-config.ts +198 -0
- package/src/detectors/detect-database.ts +22 -0
- package/src/detectors/detect-framework.ts +23 -0
- package/src/detectors/detect-project-info.ts +18 -0
- package/src/detectors/detect-runtime.ts +31 -0
- package/src/detectors/detect-system-info.ts +209 -0
- package/src/index.ts +88 -0
- package/src/project-id.ts +27 -0
- package/src/telemetry.test.ts +321 -0
- package/src/types.ts +72 -0
- package/src/utils/hash.ts +9 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/import-util.ts +3 -0
- package/src/utils/package-json.ts +74 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createTelemetry } from "./index";
|
|
3
|
+
import type { TelemetryEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
vi.mock("@better-fetch/fetch", () => ({
|
|
6
|
+
betterFetch: vi.fn(async () => ({ status: 200 })),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("./project-id", () => ({
|
|
10
|
+
getProjectId: vi.fn(async () => "anon-123"),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("./detectors/detect-runtime", () => ({
|
|
14
|
+
detectRuntime: vi.fn(() => ({ name: "node", version: "test" })),
|
|
15
|
+
detectEnvironment: vi.fn(() => "test"),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("./detectors/detect-database", () => ({
|
|
19
|
+
detectDatabase: vi.fn(async () => ({ name: "postgresql", version: "1.0.0" })),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("./detectors/detect-framework", () => ({
|
|
23
|
+
detectFramework: vi.fn(async () => ({ name: "next", version: "15.0.0" })),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("./detectors/detect-system-info", () => ({
|
|
27
|
+
detectSystemInfo: vi.fn(() => ({
|
|
28
|
+
systemPlatform: "darwin",
|
|
29
|
+
systemRelease: "24.6.0",
|
|
30
|
+
systemArchitecture: "arm64",
|
|
31
|
+
cpuCount: 8,
|
|
32
|
+
cpuModel: "Apple M3",
|
|
33
|
+
cpuSpeed: 3200,
|
|
34
|
+
memory: 16 * 1024 * 1024 * 1024,
|
|
35
|
+
isDocker: false,
|
|
36
|
+
isTTY: true,
|
|
37
|
+
isWSL: false,
|
|
38
|
+
isCI: false,
|
|
39
|
+
})),
|
|
40
|
+
isCI: vi.fn(() => false),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("./detectors/detect-project-info", () => ({
|
|
44
|
+
detectPackageManager: vi.fn(() => ({ name: "pnpm", version: "9.0.0" })),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules();
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
process.env.BETTER_AUTH_TELEMETRY = "";
|
|
51
|
+
process.env.BETTER_AUTH_TELEMETRY_DEBUG = "";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("telemetry", () => {
|
|
55
|
+
it("publishes events when enabled", async () => {
|
|
56
|
+
let event: TelemetryEvent | undefined;
|
|
57
|
+
const track = vi.fn().mockImplementation(async (e) => {
|
|
58
|
+
event = e;
|
|
59
|
+
});
|
|
60
|
+
await createTelemetry(
|
|
61
|
+
{
|
|
62
|
+
baseURL: "http://localhost.com", //this shouldn't be tracked
|
|
63
|
+
appName: "test", //this shouldn't be tracked
|
|
64
|
+
advanced: {
|
|
65
|
+
cookiePrefix: "test", //this shouldn't be tracked - should set to true
|
|
66
|
+
crossSubDomainCookies: {
|
|
67
|
+
domain: ".test.com", //this shouldn't be tracked - should set to true
|
|
68
|
+
enabled: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
telemetry: { enabled: true },
|
|
72
|
+
},
|
|
73
|
+
{ customTrack: track, skipTestCheck: true },
|
|
74
|
+
);
|
|
75
|
+
expect(event).toMatchObject({
|
|
76
|
+
type: "init",
|
|
77
|
+
payload: {
|
|
78
|
+
config: {
|
|
79
|
+
emailVerification: {
|
|
80
|
+
sendVerificationEmail: false,
|
|
81
|
+
sendOnSignUp: false,
|
|
82
|
+
sendOnSignIn: false,
|
|
83
|
+
autoSignInAfterVerification: false,
|
|
84
|
+
expiresIn: undefined,
|
|
85
|
+
onEmailVerification: false,
|
|
86
|
+
afterEmailVerification: false,
|
|
87
|
+
},
|
|
88
|
+
emailAndPassword: {
|
|
89
|
+
enabled: false,
|
|
90
|
+
disableSignUp: false,
|
|
91
|
+
requireEmailVerification: false,
|
|
92
|
+
maxPasswordLength: undefined,
|
|
93
|
+
minPasswordLength: undefined,
|
|
94
|
+
sendResetPassword: false,
|
|
95
|
+
resetPasswordTokenExpiresIn: undefined,
|
|
96
|
+
onPasswordReset: false,
|
|
97
|
+
password: { hash: false, verify: false },
|
|
98
|
+
autoSignIn: false,
|
|
99
|
+
revokeSessionsOnPasswordReset: false,
|
|
100
|
+
},
|
|
101
|
+
socialProviders: [],
|
|
102
|
+
plugins: undefined,
|
|
103
|
+
user: {
|
|
104
|
+
modelName: undefined,
|
|
105
|
+
fields: undefined,
|
|
106
|
+
additionalFields: undefined,
|
|
107
|
+
changeEmail: {
|
|
108
|
+
enabled: undefined,
|
|
109
|
+
sendChangeEmailVerification: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
verification: {
|
|
113
|
+
modelName: undefined,
|
|
114
|
+
disableCleanup: undefined,
|
|
115
|
+
fields: undefined,
|
|
116
|
+
},
|
|
117
|
+
session: {
|
|
118
|
+
modelName: undefined,
|
|
119
|
+
additionalFields: undefined,
|
|
120
|
+
cookieCache: { enabled: undefined, maxAge: undefined },
|
|
121
|
+
disableSessionRefresh: undefined,
|
|
122
|
+
expiresIn: undefined,
|
|
123
|
+
fields: undefined,
|
|
124
|
+
freshAge: undefined,
|
|
125
|
+
preserveSessionInDatabase: undefined,
|
|
126
|
+
storeSessionInDatabase: undefined,
|
|
127
|
+
updateAge: undefined,
|
|
128
|
+
},
|
|
129
|
+
account: {
|
|
130
|
+
modelName: undefined,
|
|
131
|
+
fields: undefined,
|
|
132
|
+
encryptOAuthTokens: undefined,
|
|
133
|
+
updateAccountOnSignIn: undefined,
|
|
134
|
+
accountLinking: {
|
|
135
|
+
enabled: undefined,
|
|
136
|
+
trustedProviders: undefined,
|
|
137
|
+
updateUserInfoOnLink: undefined,
|
|
138
|
+
allowUnlinkingAll: undefined,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
hooks: { after: false, before: false },
|
|
142
|
+
secondaryStorage: false,
|
|
143
|
+
advanced: {
|
|
144
|
+
cookiePrefix: true,
|
|
145
|
+
cookies: false,
|
|
146
|
+
crossSubDomainCookies: {
|
|
147
|
+
domain: true,
|
|
148
|
+
enabled: true,
|
|
149
|
+
additionalCookies: undefined,
|
|
150
|
+
},
|
|
151
|
+
database: {
|
|
152
|
+
useNumberId: false,
|
|
153
|
+
generateId: undefined,
|
|
154
|
+
defaultFindManyLimit: undefined,
|
|
155
|
+
},
|
|
156
|
+
useSecureCookies: undefined,
|
|
157
|
+
ipAddress: {
|
|
158
|
+
disableIpTracking: undefined,
|
|
159
|
+
ipAddressHeaders: undefined,
|
|
160
|
+
},
|
|
161
|
+
disableCSRFCheck: undefined,
|
|
162
|
+
cookieAttributes: {
|
|
163
|
+
expires: undefined,
|
|
164
|
+
secure: undefined,
|
|
165
|
+
sameSite: undefined,
|
|
166
|
+
domain: false,
|
|
167
|
+
path: undefined,
|
|
168
|
+
httpOnly: undefined,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
trustedOrigins: undefined,
|
|
172
|
+
rateLimit: {
|
|
173
|
+
storage: undefined,
|
|
174
|
+
modelName: undefined,
|
|
175
|
+
window: undefined,
|
|
176
|
+
customStorage: false,
|
|
177
|
+
enabled: undefined,
|
|
178
|
+
max: undefined,
|
|
179
|
+
},
|
|
180
|
+
onAPIError: {
|
|
181
|
+
errorURL: undefined,
|
|
182
|
+
onError: false,
|
|
183
|
+
throw: undefined,
|
|
184
|
+
},
|
|
185
|
+
logger: { disabled: undefined, level: undefined, log: false },
|
|
186
|
+
databaseHooks: {
|
|
187
|
+
user: {
|
|
188
|
+
create: {
|
|
189
|
+
after: false,
|
|
190
|
+
before: false,
|
|
191
|
+
},
|
|
192
|
+
update: {
|
|
193
|
+
after: false,
|
|
194
|
+
before: false,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
session: {
|
|
198
|
+
create: {
|
|
199
|
+
after: false,
|
|
200
|
+
before: false,
|
|
201
|
+
},
|
|
202
|
+
update: {
|
|
203
|
+
after: false,
|
|
204
|
+
before: false,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
account: {
|
|
208
|
+
create: {
|
|
209
|
+
after: false,
|
|
210
|
+
before: false,
|
|
211
|
+
},
|
|
212
|
+
update: {
|
|
213
|
+
after: false,
|
|
214
|
+
before: false,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
verification: {
|
|
218
|
+
create: {
|
|
219
|
+
after: false,
|
|
220
|
+
before: false,
|
|
221
|
+
},
|
|
222
|
+
update: {
|
|
223
|
+
after: false,
|
|
224
|
+
before: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
runtime: { name: "node", version: "test" },
|
|
230
|
+
database: { name: "postgresql", version: "1.0.0" },
|
|
231
|
+
framework: { name: "next", version: "15.0.0" },
|
|
232
|
+
environment: "test",
|
|
233
|
+
systemInfo: {
|
|
234
|
+
systemPlatform: "darwin",
|
|
235
|
+
systemRelease: "24.6.0",
|
|
236
|
+
systemArchitecture: "arm64",
|
|
237
|
+
cpuCount: 8,
|
|
238
|
+
cpuModel: "Apple M3",
|
|
239
|
+
cpuSpeed: 3200,
|
|
240
|
+
memory: 17179869184,
|
|
241
|
+
isDocker: false,
|
|
242
|
+
isTTY: true,
|
|
243
|
+
isWSL: false,
|
|
244
|
+
isCI: false,
|
|
245
|
+
},
|
|
246
|
+
packageManager: { name: "pnpm", version: "9.0.0" },
|
|
247
|
+
},
|
|
248
|
+
anonymousId: "anon-123",
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("does not publish when disabled via env", async () => {
|
|
253
|
+
process.env.BETTER_AUTH_TELEMETRY = "false";
|
|
254
|
+
let event: TelemetryEvent | undefined;
|
|
255
|
+
const track = vi.fn().mockImplementation(async (e) => {
|
|
256
|
+
event = e;
|
|
257
|
+
});
|
|
258
|
+
await createTelemetry(
|
|
259
|
+
{
|
|
260
|
+
baseURL: "http://localhost",
|
|
261
|
+
},
|
|
262
|
+
{ customTrack: track, skipTestCheck: true },
|
|
263
|
+
);
|
|
264
|
+
expect(event).toBeUndefined();
|
|
265
|
+
expect(track).not.toBeCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("does not publish when disabled via option", async () => {
|
|
269
|
+
let event: TelemetryEvent | undefined;
|
|
270
|
+
const track = vi.fn().mockImplementation(async (e) => {
|
|
271
|
+
event = e;
|
|
272
|
+
});
|
|
273
|
+
await createTelemetry(
|
|
274
|
+
{
|
|
275
|
+
baseURL: "http://localhost",
|
|
276
|
+
telemetry: { enabled: false },
|
|
277
|
+
},
|
|
278
|
+
{ customTrack: track, skipTestCheck: true },
|
|
279
|
+
);
|
|
280
|
+
expect(event).toBeUndefined();
|
|
281
|
+
expect(track).not.toBeCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("shouldn't fail cause track isn't being reached", async () => {
|
|
285
|
+
await expect(
|
|
286
|
+
createTelemetry(
|
|
287
|
+
{
|
|
288
|
+
baseURL: "http://localhost",
|
|
289
|
+
telemetry: { enabled: true },
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
customTrack() {
|
|
293
|
+
throw new Error("test");
|
|
294
|
+
},
|
|
295
|
+
skipTestCheck: true,
|
|
296
|
+
},
|
|
297
|
+
),
|
|
298
|
+
).resolves.not.throw(Error);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("initializes without Node built-ins in edge-like env (no process.cwd)", async () => {
|
|
302
|
+
const originalProcess = globalThis.process;
|
|
303
|
+
try {
|
|
304
|
+
// Simulate an edge runtime where process exists minimally but has no cwd
|
|
305
|
+
// so utils/package-json won't try to import fs/path
|
|
306
|
+
(globalThis as any).process = { env: {} } as any;
|
|
307
|
+
const track = vi.fn();
|
|
308
|
+
await expect(
|
|
309
|
+
createTelemetry(
|
|
310
|
+
{ baseURL: "https://example.com", telemetry: { enabled: true } },
|
|
311
|
+
{ customTrack: track, skipTestCheck: true },
|
|
312
|
+
),
|
|
313
|
+
).resolves.not.toThrow();
|
|
314
|
+
// Should still attempt to publish init event
|
|
315
|
+
expect(track).toHaveBeenCalled();
|
|
316
|
+
} finally {
|
|
317
|
+
// restore
|
|
318
|
+
(globalThis as any).process = originalProcess as any;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface DetectionInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SystemInfo {
|
|
7
|
+
// Software information
|
|
8
|
+
systemPlatform: string;
|
|
9
|
+
systemRelease: string;
|
|
10
|
+
systemArchitecture: string;
|
|
11
|
+
|
|
12
|
+
// Machine information
|
|
13
|
+
cpuCount: number;
|
|
14
|
+
cpuModel: string | null;
|
|
15
|
+
cpuSpeed: number | null;
|
|
16
|
+
memory: number;
|
|
17
|
+
|
|
18
|
+
// Environment information
|
|
19
|
+
isDocker: boolean;
|
|
20
|
+
isTTY: boolean;
|
|
21
|
+
isWSL: boolean;
|
|
22
|
+
isCI: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthConfigInfo {
|
|
26
|
+
options: any;
|
|
27
|
+
plugins: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProjectInfo {
|
|
31
|
+
isGit: boolean;
|
|
32
|
+
packageManager: DetectionInfo | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TelemetryEvent {
|
|
36
|
+
type: string;
|
|
37
|
+
anonymousId?: string;
|
|
38
|
+
payload: Record<string, any>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TelemetryContext {
|
|
42
|
+
customTrack?: (event: TelemetryEvent) => Promise<void>;
|
|
43
|
+
database?: string;
|
|
44
|
+
adapter?: string;
|
|
45
|
+
skipTestCheck?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Minimal interface for BetterAuth options to avoid circular dependencies
|
|
49
|
+
export interface BetterAuthOptions {
|
|
50
|
+
baseURL?: string;
|
|
51
|
+
appName?: string;
|
|
52
|
+
telemetry?: {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
debug?: boolean;
|
|
55
|
+
};
|
|
56
|
+
emailVerification?: any;
|
|
57
|
+
emailAndPassword?: any;
|
|
58
|
+
socialProviders?: Record<string, any>;
|
|
59
|
+
plugins?: Array<{ id: string | symbol }>;
|
|
60
|
+
user?: any;
|
|
61
|
+
verification?: any;
|
|
62
|
+
session?: any;
|
|
63
|
+
account?: any;
|
|
64
|
+
hooks?: any;
|
|
65
|
+
secondaryStorage?: any;
|
|
66
|
+
advanced?: any;
|
|
67
|
+
trustedOrigins?: any;
|
|
68
|
+
rateLimit?: any;
|
|
69
|
+
onAPIError?: any;
|
|
70
|
+
logger?: any;
|
|
71
|
+
databaseHooks?: any;
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
2
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
3
|
+
|
|
4
|
+
export async function hashToBase64(
|
|
5
|
+
data: string | ArrayBuffer,
|
|
6
|
+
): Promise<string> {
|
|
7
|
+
const buffer = await createHash("SHA-256").digest(data);
|
|
8
|
+
return base64.encode(buffer);
|
|
9
|
+
}
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PackageJson } from "type-fest";
|
|
2
|
+
let packageJSONCache: PackageJson | undefined;
|
|
3
|
+
|
|
4
|
+
async function readRootPackageJson() {
|
|
5
|
+
if (packageJSONCache) return packageJSONCache;
|
|
6
|
+
try {
|
|
7
|
+
const cwd =
|
|
8
|
+
typeof process !== "undefined" && typeof process.cwd === "function"
|
|
9
|
+
? process.cwd()
|
|
10
|
+
: "";
|
|
11
|
+
if (!cwd) return undefined;
|
|
12
|
+
// Lazily import Node built-ins only when available (Node/Bun/Deno) and
|
|
13
|
+
// avoid static analyzer/bundler resolution by obfuscating module names
|
|
14
|
+
const importRuntime = (m: string) =>
|
|
15
|
+
(Function("mm", "return import(mm)") as any)(m);
|
|
16
|
+
const [{ default: fs }, { default: path }] = await Promise.all([
|
|
17
|
+
importRuntime("fs/promises"),
|
|
18
|
+
importRuntime("path"),
|
|
19
|
+
]);
|
|
20
|
+
const raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8");
|
|
21
|
+
packageJSONCache = JSON.parse(raw);
|
|
22
|
+
return packageJSONCache as PackageJson;
|
|
23
|
+
} catch {}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getPackageVersion(pkg: string) {
|
|
28
|
+
if (packageJSONCache) {
|
|
29
|
+
return (packageJSONCache.dependencies?.[pkg] ||
|
|
30
|
+
packageJSONCache.devDependencies?.[pkg] ||
|
|
31
|
+
packageJSONCache.peerDependencies?.[pkg]) as string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const cwd =
|
|
36
|
+
typeof process !== "undefined" && typeof process.cwd === "function"
|
|
37
|
+
? process.cwd()
|
|
38
|
+
: "";
|
|
39
|
+
if (!cwd) throw new Error("no-cwd");
|
|
40
|
+
const importRuntime = (m: string) =>
|
|
41
|
+
(Function("mm", "return import(mm)") as any)(m);
|
|
42
|
+
const [{ default: fs }, { default: path }] = await Promise.all([
|
|
43
|
+
importRuntime("fs/promises"),
|
|
44
|
+
importRuntime("path"),
|
|
45
|
+
]);
|
|
46
|
+
const pkgJsonPath = path.join(cwd, "node_modules", pkg, "package.json");
|
|
47
|
+
const raw = await fs.readFile(pkgJsonPath, "utf-8");
|
|
48
|
+
const json = JSON.parse(raw);
|
|
49
|
+
const resolved =
|
|
50
|
+
(json.version as string) ||
|
|
51
|
+
(await getVersionFromLocalPackageJson(pkg)) ||
|
|
52
|
+
undefined;
|
|
53
|
+
return resolved;
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
const fromRoot = await getVersionFromLocalPackageJson(pkg);
|
|
57
|
+
return fromRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getVersionFromLocalPackageJson(pkg: string) {
|
|
61
|
+
const json = await readRootPackageJson();
|
|
62
|
+
if (!json) return undefined;
|
|
63
|
+
const allDeps = {
|
|
64
|
+
...json.dependencies,
|
|
65
|
+
...json.devDependencies,
|
|
66
|
+
...json.peerDependencies,
|
|
67
|
+
} as Record<string, string | undefined>;
|
|
68
|
+
return allDeps[pkg];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getNameFromLocalPackageJson() {
|
|
72
|
+
const json = await readRootPackageJson();
|
|
73
|
+
return json?.name as string | undefined;
|
|
74
|
+
}
|