@agent-native/core 0.84.21 → 0.84.22
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +27 -4
- package/corpus/core/src/client/guided-questions.tsx +11 -6
- package/corpus/core/src/client/tool-display.ts +8 -0
- package/corpus/core/src/file-upload/builder.ts +7 -2
- package/corpus/core/src/notifications/channels.ts +235 -38
- package/corpus/core/src/server/analytics.ts +32 -6
- package/corpus/core/src/server/ssr-handler.ts +31 -12
- package/corpus/templates/analytics/AGENTS.md +21 -0
- package/corpus/templates/analytics/README.md +35 -0
- package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
- package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
- package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
- package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
- package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
- package/corpus/templates/analytics/netlify.toml +3 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/db/schema.ts +55 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
- package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
- package/corpus/templates/analytics/server/plugins/db.ts +100 -0
- package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
- package/corpus/templates/assets/README.md +30 -8
- package/corpus/templates/brain/README.md +20 -552
- package/corpus/templates/calendar/README.md +22 -28
- package/corpus/templates/chat/README.md +32 -0
- package/corpus/templates/clips/README.md +37 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
- package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +41 -1
- package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +37 -0
- package/corpus/templates/content/AGENTS.md +7 -3
- package/corpus/templates/content/README.md +22 -34
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
- package/corpus/templates/content/app/i18n-data.ts +278 -40
- package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/AGENTS.md +7 -6
- package/corpus/templates/design/README.md +33 -0
- package/corpus/templates/design/actions/delete-file.ts +86 -10
- package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
- package/corpus/templates/design/actions/present-design-variants.ts +11 -13
- package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
- package/corpus/templates/dispatch/README.md +34 -0
- package/corpus/templates/forms/README.md +34 -0
- package/corpus/templates/macros/README.md +28 -0
- package/corpus/templates/mail/README.md +35 -0
- package/corpus/templates/plan/README.md +30 -118
- package/corpus/templates/slides/README.md +34 -0
- package/corpus/templates/videos/README.md +22 -201
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +28 -4
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +10 -6
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/tool-display.d.ts.map +1 -1
- package/dist/client/tool-display.js +8 -0
- package/dist/client/tool-display.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +6 -2
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/notifications/channels.d.ts +10 -0
- package/dist/notifications/channels.d.ts.map +1 -1
- package/dist/notifications/channels.js +172 -20
- package/dist/notifications/channels.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +16 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +30 -7
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +28 -12
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { deleteAnalyticsAlertRule } from "../server/lib/analytics-alerts";
|
|
9
|
+
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description: "Delete a reusable first-party analytics alert rule.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
id: z.string().describe("Alert rule ID"),
|
|
14
|
+
}),
|
|
15
|
+
http: { method: "POST" },
|
|
16
|
+
run: async ({ id }) => {
|
|
17
|
+
const email = getRequestUserEmail();
|
|
18
|
+
if (!email) throw new Error("no authenticated user");
|
|
19
|
+
const orgId = getRequestOrgId() || null;
|
|
20
|
+
await deleteAnalyticsAlertRule(id, { email, orgId });
|
|
21
|
+
return { ok: true };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { listAnalyticsAlertRules } from "../server/lib/analytics-alerts";
|
|
9
|
+
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description: "List reusable alert rules over first-party analytics events.",
|
|
12
|
+
schema: z.object({}),
|
|
13
|
+
http: { method: "GET" },
|
|
14
|
+
run: async () => {
|
|
15
|
+
const email = getRequestUserEmail();
|
|
16
|
+
if (!email) throw new Error("no authenticated user");
|
|
17
|
+
const orgId = getRequestOrgId() || null;
|
|
18
|
+
return listAnalyticsAlertRules({ email, orgId });
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { runAnalyticsAlertsOnce } from "../server/jobs/analytics-alerts";
|
|
9
|
+
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description:
|
|
12
|
+
"Run one alert sweep for the current user's first-party analytics alert rules.",
|
|
13
|
+
schema: z.object({
|
|
14
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
15
|
+
}),
|
|
16
|
+
http: { method: "POST" },
|
|
17
|
+
run: async ({ limit }) => {
|
|
18
|
+
const email = getRequestUserEmail();
|
|
19
|
+
if (!email) throw new Error("no authenticated user");
|
|
20
|
+
const orgId = getRequestOrgId() || null;
|
|
21
|
+
return runAnalyticsAlertsOnce({ ownerEmail: email, orgId, limit });
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
import { saveAnalyticsAlertRule } from "../server/lib/analytics-alerts";
|
|
9
|
+
|
|
10
|
+
const filterSchema = z.object({
|
|
11
|
+
field: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe(
|
|
14
|
+
"Analytics field path, e.g. event_name, app, template, properties.error_code, or context.url.",
|
|
15
|
+
),
|
|
16
|
+
op: z
|
|
17
|
+
.enum(["equals", "not_equals", "contains", "in", "exists"])
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Filter operator; defaults to equals."),
|
|
20
|
+
value: z.unknown().optional().describe("Value to compare against."),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default defineAction({
|
|
24
|
+
description:
|
|
25
|
+
"Create or update a reusable alert rule over first-party analytics events.",
|
|
26
|
+
schema: z.object({
|
|
27
|
+
id: z.string().optional().describe("Existing alert rule ID to update"),
|
|
28
|
+
name: z.string().describe("Human-readable alert name"),
|
|
29
|
+
description: z.string().optional(),
|
|
30
|
+
eventName: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.nullable()
|
|
34
|
+
.describe("Optional exact event name to target before filters run."),
|
|
35
|
+
filters: z
|
|
36
|
+
.array(filterSchema)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Generic event filters evaluated over columns/properties."),
|
|
39
|
+
thresholdMode: z
|
|
40
|
+
.enum(["event_count", "distinct_count"])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Count matching events or distinct values."),
|
|
43
|
+
distinctBy: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.nullable()
|
|
47
|
+
.describe("Field path used when thresholdMode is distinct_count."),
|
|
48
|
+
threshold: z.number().int().min(1),
|
|
49
|
+
windowMinutes: z.number().int().min(1).max(1440),
|
|
50
|
+
cooldownMinutes: z.number().int().min(0).max(1440).optional(),
|
|
51
|
+
severity: z.enum(["warning", "critical"]).optional(),
|
|
52
|
+
channels: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Notification channels, e.g. inbox, email, slack, webhook."),
|
|
56
|
+
emailRecipients: z
|
|
57
|
+
.array(z.string().email())
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Email recipients passed to the generic email channel."),
|
|
60
|
+
enabled: z.boolean().optional(),
|
|
61
|
+
}),
|
|
62
|
+
http: { method: "POST" },
|
|
63
|
+
run: async (args) => {
|
|
64
|
+
const email = getRequestUserEmail();
|
|
65
|
+
if (!email) throw new Error("no authenticated user");
|
|
66
|
+
const orgId = getRequestOrgId() || null;
|
|
67
|
+
return saveAnalyticsAlertRule(args, { email, orgId });
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -12,6 +12,10 @@ const SCHEDULED_NAME = "dashboard-report-cron";
|
|
|
12
12
|
const WORKER_NAME = "dashboard-report-sweep-background";
|
|
13
13
|
const ROUTE_PATH = "/api/dashboard-reports/run";
|
|
14
14
|
const SCHEDULE = "*/15 * * * *";
|
|
15
|
+
const ALERT_SCHEDULED_NAME = "analytics-alert-cron";
|
|
16
|
+
const ALERT_WORKER_NAME = "analytics-alert-sweep-background";
|
|
17
|
+
const ALERT_ROUTE_PATH = "/api/analytics-alerts/run";
|
|
18
|
+
const ALERT_SCHEDULE = "*/5 * * * *";
|
|
15
19
|
|
|
16
20
|
function ensureDir(dir: string) {
|
|
17
21
|
mkdirSync(dir, { recursive: true });
|
|
@@ -116,6 +120,105 @@ export const config = {
|
|
|
116
120
|
writeFileSync(path.join(dest, `${WORKER_NAME}.mjs`), source);
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
function emitAlertScheduledTrigger(token: string) {
|
|
124
|
+
const dest = path.join(FUNCTIONS_DIR, ALERT_SCHEDULED_NAME);
|
|
125
|
+
rmSync(dest, { recursive: true, force: true });
|
|
126
|
+
ensureDir(dest);
|
|
127
|
+
|
|
128
|
+
const source = `const WORKER_PATH = "/.netlify/functions/${ALERT_WORKER_NAME}";
|
|
129
|
+
const CRON_TOKEN = ${JSON.stringify(token)};
|
|
130
|
+
|
|
131
|
+
function siteOrigin(request) {
|
|
132
|
+
const configured = process.env.URL || process.env.DEPLOY_URL;
|
|
133
|
+
if (configured) return configured;
|
|
134
|
+
const url = new URL(request.url);
|
|
135
|
+
return url.origin;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default async function handler(request) {
|
|
139
|
+
const url = new URL(WORKER_PATH, siteOrigin(request));
|
|
140
|
+
const response = await fetch(url.toString(), {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
"x-agent-native-analytics-alert-cron": CRON_TOKEN,
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({ scheduled: true }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!response.ok && response.status !== 202) {
|
|
150
|
+
console.error(
|
|
151
|
+
"[analytics-alert-cron] Background sweep trigger failed:",
|
|
152
|
+
response.status,
|
|
153
|
+
await response.text().catch(() => ""),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const config = {
|
|
159
|
+
name: "analytics alert cron trigger",
|
|
160
|
+
generator: "agent-native analytics build",
|
|
161
|
+
schedule: ${JSON.stringify(ALERT_SCHEDULE)},
|
|
162
|
+
};
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
writeFileSync(path.join(dest, `${ALERT_SCHEDULED_NAME}.mjs`), source);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emitAlertBackgroundWorker(token: string) {
|
|
169
|
+
const dest = path.join(FUNCTIONS_DIR, ALERT_WORKER_NAME);
|
|
170
|
+
rmSync(dest, { recursive: true, force: true });
|
|
171
|
+
cpSync(SERVER_DIR, dest, { recursive: true });
|
|
172
|
+
rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
173
|
+
|
|
174
|
+
const source = `globalThis.__AGENT_NATIVE_ANALYTICS_ALERT_SCHEDULED_RUNTIME__ = true;
|
|
175
|
+
|
|
176
|
+
const CRON_TOKEN = ${JSON.stringify(token)};
|
|
177
|
+
const ROUTE_PATH = ${JSON.stringify(ALERT_ROUTE_PATH)};
|
|
178
|
+
let cachedHandler;
|
|
179
|
+
|
|
180
|
+
function timingSafeEquals(a, b) {
|
|
181
|
+
if (a.length !== b.length) return false;
|
|
182
|
+
let diff = 0;
|
|
183
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
184
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
185
|
+
}
|
|
186
|
+
return diff === 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default async function handler(request, context) {
|
|
190
|
+
const token = request.headers.get("x-agent-native-analytics-alert-cron") || "";
|
|
191
|
+
if (!timingSafeEquals(token, CRON_TOKEN)) {
|
|
192
|
+
return new Response("Unauthorized", { status: 401 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
196
|
+
const url = new URL(request.url);
|
|
197
|
+
url.pathname = ROUTE_PATH;
|
|
198
|
+
url.search = "";
|
|
199
|
+
|
|
200
|
+
const rewritten = new Request(url.toString(), {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
203
|
+
body: JSON.stringify({ scheduled: true }),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return await cachedHandler(rewritten, context);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const config = {
|
|
210
|
+
name: "analytics alert background sweep",
|
|
211
|
+
generator: "agent-native analytics build",
|
|
212
|
+
background: true,
|
|
213
|
+
nodeBundler: "none",
|
|
214
|
+
includedFiles: ["**"],
|
|
215
|
+
preferStatic: false,
|
|
216
|
+
};
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
writeFileSync(path.join(dest, `${ALERT_WORKER_NAME}.mjs`), source);
|
|
220
|
+
}
|
|
221
|
+
|
|
119
222
|
function isDirectRun(): boolean {
|
|
120
223
|
const entrypoint = process.argv[1];
|
|
121
224
|
return Boolean(
|
|
@@ -133,11 +236,17 @@ function main(): void {
|
|
|
133
236
|
}
|
|
134
237
|
|
|
135
238
|
const token = randomBytes(32).toString("hex");
|
|
239
|
+
const alertToken = randomBytes(32).toString("hex");
|
|
136
240
|
emitScheduledTrigger(token);
|
|
137
241
|
emitBackgroundWorker(token);
|
|
242
|
+
emitAlertScheduledTrigger(alertToken);
|
|
243
|
+
emitAlertBackgroundWorker(alertToken);
|
|
138
244
|
console.log(
|
|
139
245
|
`[dashboard-report-cron] Emitted Netlify scheduled trigger "${SCHEDULED_NAME}" (${SCHEDULE}) and background worker "${WORKER_NAME}".`,
|
|
140
246
|
);
|
|
247
|
+
console.log(
|
|
248
|
+
`[analytics-alert-cron] Emitted Netlify scheduled trigger "${ALERT_SCHEDULED_NAME}" (${ALERT_SCHEDULE}) and background worker "${ALERT_WORKER_NAME}".`,
|
|
249
|
+
);
|
|
141
250
|
}
|
|
142
251
|
|
|
143
252
|
if (isDirectRun()) {
|
|
@@ -179,6 +179,61 @@ export const analyticsEvents = table("analytics_events", {
|
|
|
179
179
|
orgId: text("org_id"),
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Generic alert rules over first-party analytics events. Rules are owned by a
|
|
184
|
+
* user/org but can target any app, template, event name, or event property.
|
|
185
|
+
*/
|
|
186
|
+
export const analyticsAlertRules = table("analytics_alert_rules", {
|
|
187
|
+
id: text("id").primaryKey(),
|
|
188
|
+
name: text("name").notNull(),
|
|
189
|
+
description: text("description").notNull().default(""),
|
|
190
|
+
eventName: text("event_name"),
|
|
191
|
+
filters: text("filters").notNull().default("[]"),
|
|
192
|
+
thresholdMode: text("threshold_mode", {
|
|
193
|
+
enum: ["event_count", "distinct_count"],
|
|
194
|
+
})
|
|
195
|
+
.notNull()
|
|
196
|
+
.default("event_count"),
|
|
197
|
+
distinctBy: text("distinct_by"),
|
|
198
|
+
threshold: integer("threshold").notNull().default(1),
|
|
199
|
+
windowMinutes: integer("window_minutes").notNull().default(10),
|
|
200
|
+
cooldownMinutes: integer("cooldown_minutes").notNull().default(30),
|
|
201
|
+
severity: text("severity", { enum: ["warning", "critical"] })
|
|
202
|
+
.notNull()
|
|
203
|
+
.default("warning"),
|
|
204
|
+
channels: text("channels").notNull().default('["inbox"]'),
|
|
205
|
+
emailRecipients: text("email_recipients").notNull().default("[]"),
|
|
206
|
+
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
207
|
+
lastEvaluatedAt: text("last_evaluated_at"),
|
|
208
|
+
lastTriggeredAt: text("last_triggered_at"),
|
|
209
|
+
lastStatus: text("last_status", {
|
|
210
|
+
enum: ["ok", "triggered", "cooldown", "error"],
|
|
211
|
+
}),
|
|
212
|
+
lastError: text("last_error"),
|
|
213
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
214
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
215
|
+
ownerEmail: text("owner_email").notNull().default("local@localhost"),
|
|
216
|
+
orgId: text("org_id"),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export const analyticsAlertIncidents = table("analytics_alert_incidents", {
|
|
220
|
+
id: text("id").primaryKey(),
|
|
221
|
+
ruleId: text("rule_id").notNull(),
|
|
222
|
+
triggeredAt: text("triggered_at").notNull(),
|
|
223
|
+
windowStart: text("window_start").notNull(),
|
|
224
|
+
windowEnd: text("window_end").notNull(),
|
|
225
|
+
threshold: integer("threshold").notNull(),
|
|
226
|
+
observedValue: integer("observed_value").notNull(),
|
|
227
|
+
eventCount: integer("event_count").notNull(),
|
|
228
|
+
severity: text("severity", { enum: ["warning", "critical"] }).notNull(),
|
|
229
|
+
channels: text("channels").notNull().default("[]"),
|
|
230
|
+
sampleEvents: text("sample_events").notNull().default("[]"),
|
|
231
|
+
notificationId: text("notification_id"),
|
|
232
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
233
|
+
ownerEmail: text("owner_email").notNull().default("local@localhost"),
|
|
234
|
+
orgId: text("org_id"),
|
|
235
|
+
});
|
|
236
|
+
|
|
182
237
|
/**
|
|
183
238
|
* Session replay summaries recorded through the first-party analytics replay
|
|
184
239
|
* endpoint. Raw replay chunks live in session_replay_chunks and are only read
|
|
@@ -355,12 +355,15 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
355
355
|
userEmail: ctx.userEmail,
|
|
356
356
|
orgId: ctx.orgId ?? null,
|
|
357
357
|
});
|
|
358
|
+
// Serve decompressed JSON and let the platform negotiate wire
|
|
359
|
+
// compression. Manually returning a pre-gzipped body with a
|
|
360
|
+
// `Content-Encoding: gzip` header corrupted replay downloads on
|
|
361
|
+
// serverless hosts and left playback blank in production.
|
|
358
362
|
setResponseHeader(event, "Content-Type", "application/json");
|
|
359
|
-
setResponseHeader(event, "Content-Encoding", "gzip");
|
|
360
363
|
setResponseHeader(event, "Cache-Control", "no-store");
|
|
361
364
|
setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
|
|
362
365
|
setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
|
|
363
|
-
return result.
|
|
366
|
+
return result.json;
|
|
364
367
|
} catch (error: any) {
|
|
365
368
|
setResponseStatus(event, statusFromError(error));
|
|
366
369
|
return { error: messageFromError(error) };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateAndNotifyAnalyticsAlertRule,
|
|
3
|
+
listEnabledAnalyticsAlertRules,
|
|
4
|
+
markAnalyticsAlertRuleError,
|
|
5
|
+
} from "../lib/analytics-alerts";
|
|
6
|
+
|
|
7
|
+
let running = false;
|
|
8
|
+
const DEFAULT_MAX_RULES_PER_SWEEP = 100;
|
|
9
|
+
|
|
10
|
+
function maxRulesPerSweep(input?: number): number {
|
|
11
|
+
if (input) return Math.max(1, Math.min(500, Math.floor(input)));
|
|
12
|
+
const raw = process.env.ANALYTICS_ALERT_SWEEP_LIMIT?.trim();
|
|
13
|
+
if (!raw) return DEFAULT_MAX_RULES_PER_SWEEP;
|
|
14
|
+
const parsed = Number.parseInt(raw, 10);
|
|
15
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
16
|
+
? parsed
|
|
17
|
+
: DEFAULT_MAX_RULES_PER_SWEEP;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run one analytics alert sweep. Exported for deployment-specific scheduled
|
|
22
|
+
* functions that should not rely on a long-lived Node process.
|
|
23
|
+
*/
|
|
24
|
+
export async function runAnalyticsAlertsOnce(
|
|
25
|
+
options: {
|
|
26
|
+
ownerEmail?: string;
|
|
27
|
+
orgId?: string | null;
|
|
28
|
+
limit?: number;
|
|
29
|
+
} = {},
|
|
30
|
+
): Promise<{
|
|
31
|
+
processed: number;
|
|
32
|
+
triggered: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
remaining: number;
|
|
35
|
+
}> {
|
|
36
|
+
if (running) return { processed: 0, triggered: 0, failed: 0, remaining: 0 };
|
|
37
|
+
running = true;
|
|
38
|
+
let processed = 0;
|
|
39
|
+
let triggered = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
let remaining = 0;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const sweepLimit = maxRulesPerSweep(options.limit);
|
|
45
|
+
const rules = await listEnabledAnalyticsAlertRules({
|
|
46
|
+
limit: sweepLimit,
|
|
47
|
+
ownerEmail: options.ownerEmail,
|
|
48
|
+
orgId: options.orgId,
|
|
49
|
+
});
|
|
50
|
+
remaining = rules.length >= sweepLimit ? 1 : 0;
|
|
51
|
+
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
processed++;
|
|
54
|
+
try {
|
|
55
|
+
const result = await evaluateAndNotifyAnalyticsAlertRule(rule);
|
|
56
|
+
if (result.status === "triggered") triggered++;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
failed++;
|
|
59
|
+
await markAnalyticsAlertRuleError(rule.id, err).catch(() => {});
|
|
60
|
+
console.error(
|
|
61
|
+
`[analytics-alerts] Rule ${rule.id} (${rule.name}) failed:`,
|
|
62
|
+
err,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
running = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { processed, triggered, failed, remaining };
|
|
71
|
+
}
|