-
Notifications
You must be signed in to change notification settings - Fork 189
Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images #541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
89c5b61
b9d5abe
ed9e2d1
7faac1c
1f177bd
cd7a4e1
0088484
b5644ab
76ffe29
68f8085
3eb3ddc
5da4ae8
5d6818b
3fdd4e0
c898380
f39c18f
8cc0125
077eb2f
c98bdf8
779b86b
971b1bd
b9376f6
96fc362
faa539f
c3a0be2
6fc4230
9993635
1f4c903
9972240
31cadcf
7cda26c
31059cd
96689f4
6339708
f0dca3e
7c9b0d0
195885f
b513ffb
3a624eb
708e502
24b7284
e97a886
0057544
a6b0acb
0e19bfe
88ed1f3
bca14ab
0b3115d
cc5ddf8
cb6967d
d6ca717
f4b4c6e
d909f3f
df57d87
a89f7fe
555a610
fd465f9
5a76ec8
0a2288d
7fab18d
b3fb08d
1c29ab2
8ac8c21
3b3a4ab
3ec85f4
035aeed
ae9b127
43bd8a6
7d9bdc1
635883e
df2be14
0c42b73
0ed325e
3509656
0ededd8
7559c09
ae404dc
bafeffc
f1a7e5c
5c610f4
c87c25d
a358d34
231b3d9
922653c
a3a2aac
94aa4d6
e67ecc5
b351bc0
0a52ec0
293cb3a
3b4855e
3460383
4d24e13
ea7083d
7d8c426
9918e3f
729c8dc
b853c52
492ca04
1c2c6f9
9a998d1
5872e8e
f60f31e
6e8dd30
795c618
386a858
3bceb3f
a0bb00a
d34e824
cbfb23d
1e92b0c
bc05e53
6ba6452
f8d35d4
f5f9714
957906f
f59ebaf
d74769c
342d0f5
1abc702
52f1dd8
a357b7e
0ecca50
fb0a4e1
e59e96e
b027c0d
d00a038
815dd9a
3c0c34d
2fa7288
134abf7
9124812
717a04d
acec544
e8ba85d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,16 +1,20 @@ | ||||||
| import { config } from "@databuddy/env/app"; | ||||||
| import { ValidationError } from "elysia"; | ||||||
| import { EvlogError, parseError } from "evlog"; | ||||||
| import { getRequestId } from "./request-id"; | ||||||
|
|
||||||
| interface AppErrorContext { | ||||||
| code?: string | number; | ||||||
| error: unknown; | ||||||
| request?: Request; | ||||||
| requestId?: string; | ||||||
| } | ||||||
|
|
||||||
| const HTTP_STATUS_BY_ERROR_CODE: Record<string, number> = { | ||||||
| AUTH_REQUIRED: 401, | ||||||
| BAD_REQUEST: 400, | ||||||
| CONFLICT: 409, | ||||||
| FEATURE_UNAVAILABLE: 403, | ||||||
| FEATURE_UNAVAILABLE: 402, | ||||||
| FORBIDDEN: 403, | ||||||
| INTERNAL_SERVER_ERROR: 500, | ||||||
| INVALID_COOKIE_SIGNATURE: 400, | ||||||
|
|
@@ -25,8 +29,15 @@ const HTTP_STATUS_BY_ERROR_CODE: Record<string, number> = { | |||||
| }; | ||||||
|
|
||||||
| const PROTECTED_RESOURCE_METADATA_URL = `${config.urls.api}/.well-known/oauth-protected-resource`; | ||||||
| const LEADING_SLASH_PATTERN = /^\//; | ||||||
|
|
||||||
| export function handleAppError({ error, code }: AppErrorContext) { | ||||||
| export function handleAppError({ | ||||||
| error, | ||||||
| code, | ||||||
| request, | ||||||
| requestId, | ||||||
| }: AppErrorContext) { | ||||||
| const responseRequestId = requestId ?? getRequestId(request); | ||||||
| const parsed = parseError(error); | ||||||
| const statusCode = getStatusCode({ | ||||||
| code, | ||||||
|
|
@@ -48,8 +59,10 @@ export function handleAppError({ error, code }: AppErrorContext) { | |||||
| isClientError, | ||||||
| statusCode, | ||||||
| }); | ||||||
| const validationDetails = getValidationDetails(error); | ||||||
| const headers: Record<string, string> = { | ||||||
| "Content-Type": "application/json", | ||||||
| "X-Request-ID": responseRequestId, | ||||||
| }; | ||||||
| if (statusCode === 401) { | ||||||
| headers["WWW-Authenticate"] = | ||||||
|
|
@@ -61,16 +74,55 @@ export function handleAppError({ error, code }: AppErrorContext) { | |||||
| success: false, | ||||||
| error: safeClientError, | ||||||
| code: errorCode, | ||||||
| requestId: responseRequestId, | ||||||
| ...(hasValue(parsed.why) && exposeStructured ? { why: parsed.why } : {}), | ||||||
| ...(hasValue(parsed.fix) && exposeStructured ? { fix: parsed.fix } : {}), | ||||||
| ...(hasValue(parsed.link) && exposeStructured | ||||||
| ? { link: parsed.link } | ||||||
| : {}), | ||||||
| ...(validationDetails.length > 0 ? { details: validationDetails } : {}), | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Raw Elysia validation messages are exposed unconditionally in production responses, bypassing the existing In production, Consider gating Prompt for AI agents
Suggested change
|
||||||
| }), | ||||||
| { status: statusCode, headers } | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| interface ValidationDetail { | ||||||
| field: string; | ||||||
| message: string; | ||||||
| } | ||||||
|
|
||||||
| function getValidationDetails(error: unknown): ValidationDetail[] { | ||||||
| if (!(error instanceof ValidationError) || error.type === "response") { | ||||||
| return []; | ||||||
| } | ||||||
|
|
||||||
| const details: ValidationDetail[] = []; | ||||||
| const seenFields = new Set<string>(); | ||||||
| for (const issue of error.all) { | ||||||
| const path = issue.path | ||||||
| .replace(LEADING_SLASH_PATTERN, "") | ||||||
| .split("/") | ||||||
| .filter(Boolean) | ||||||
| .join("."); | ||||||
| const field = path ? `${error.type}.${path}` : error.type; | ||||||
| if (seenFields.has(field)) { | ||||||
| continue; | ||||||
| } | ||||||
| seenFields.add(field); | ||||||
| details.push({ | ||||||
| field, | ||||||
| message: | ||||||
| typeof issue.summary === "string" && issue.summary | ||||||
| ? issue.summary | ||||||
| : issue.message, | ||||||
| }); | ||||||
| if (details.length === 20) { | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
| return details; | ||||||
| } | ||||||
|
|
||||||
| function getErrorCode({ | ||||||
| explicitCode, | ||||||
| parsedCode, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { getRequestId } from "./request-id"; | ||
|
|
||
| describe("getRequestId", () => { | ||
| it("returns a stable generated ID for the same request", () => { | ||
| const request = new Request("https://api.example.com/test"); | ||
| const requestId = getRequestId(request); | ||
|
|
||
| expect(requestId).toMatch(/^req_[a-f0-9]{16}$/); | ||
| expect(getRequestId(request)).toBe(requestId); | ||
| }); | ||
|
|
||
| it("preserves a safe caller-supplied ID", () => { | ||
| const request = new Request("https://api.example.com/test", { | ||
| headers: { "x-request-id": "trace_partner-123" }, | ||
| }); | ||
|
|
||
| expect(getRequestId(request)).toBe("trace_partner-123"); | ||
| }); | ||
|
|
||
| it("replaces unsafe caller-supplied IDs", () => { | ||
| const request = new Request("https://api.example.com/test", { | ||
| headers: { "x-request-id": "not safe for logs" }, | ||
| }); | ||
|
|
||
| expect(getRequestId(request)).toMatch(/^req_[a-f0-9]{16}$/); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/; | ||
| const requestIds = new WeakMap<Request, string>(); | ||
|
|
||
| function createRequestId(): string { | ||
| return `req_${crypto.randomUUID().replaceAll("-", "").slice(0, 16)}`; | ||
| } | ||
|
|
||
| export function getRequestId(request?: Request): string { | ||
| if (!request) { | ||
| return createRequestId(); | ||
| } | ||
|
|
||
| const existing = requestIds.get(request); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
|
|
||
| const supplied = request.headers.get("x-request-id")?.trim(); | ||
| const requestId = | ||
| supplied && REQUEST_ID_PATTERN.test(supplied) | ||
| ? supplied | ||
| : createRequestId(); | ||
| requestIds.set(request, requestId); | ||
| return requestId; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The
CLICKHOUSE_READONLY_URLsecret (which includes ClickHouse cluster credentials) is defined at the job-levelenv, so it is injected into every step in thetypecheckjob — includingbun install, docs postinstall,check-types, andch:check— even though only the finalch:verifystep connects to the live cluster. Scoping the secret to only the step that needs it reduces blast radius if any command or dependency accidentally logs environment variables.Prompt for AI agents