@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.
Files changed (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +27 -4
  5. package/corpus/core/src/client/guided-questions.tsx +11 -6
  6. package/corpus/core/src/client/tool-display.ts +8 -0
  7. package/corpus/core/src/file-upload/builder.ts +7 -2
  8. package/corpus/core/src/notifications/channels.ts +235 -38
  9. package/corpus/core/src/server/analytics.ts +32 -6
  10. package/corpus/core/src/server/ssr-handler.ts +31 -12
  11. package/corpus/templates/analytics/AGENTS.md +21 -0
  12. package/corpus/templates/analytics/README.md +35 -0
  13. package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
  14. package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
  15. package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
  16. package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
  17. package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
  19. package/corpus/templates/analytics/netlify.toml +3 -0
  20. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  21. package/corpus/templates/analytics/server/db/schema.ts +55 -0
  22. package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
  23. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
  24. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
  25. package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
  26. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
  27. package/corpus/templates/analytics/server/plugins/db.ts +100 -0
  28. package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
  29. package/corpus/templates/assets/README.md +30 -8
  30. package/corpus/templates/brain/README.md +20 -552
  31. package/corpus/templates/calendar/README.md +22 -28
  32. package/corpus/templates/chat/README.md +32 -0
  33. package/corpus/templates/clips/README.md +37 -0
  34. package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
  35. package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
  36. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +41 -1
  37. package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
  38. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  39. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +37 -0
  40. package/corpus/templates/content/AGENTS.md +7 -3
  41. package/corpus/templates/content/README.md +22 -34
  42. package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
  43. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
  44. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
  45. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
  46. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
  47. package/corpus/templates/content/app/i18n-data.ts +278 -40
  48. package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
  49. package/corpus/templates/content/shared/api.ts +1 -0
  50. package/corpus/templates/design/AGENTS.md +7 -6
  51. package/corpus/templates/design/README.md +33 -0
  52. package/corpus/templates/design/actions/delete-file.ts +86 -10
  53. package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
  54. package/corpus/templates/design/actions/present-design-variants.ts +11 -13
  55. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  56. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  57. package/corpus/templates/dispatch/README.md +34 -0
  58. package/corpus/templates/forms/README.md +34 -0
  59. package/corpus/templates/macros/README.md +28 -0
  60. package/corpus/templates/mail/README.md +35 -0
  61. package/corpus/templates/plan/README.md +30 -118
  62. package/corpus/templates/slides/README.md +34 -0
  63. package/corpus/templates/videos/README.md +22 -201
  64. package/dist/client/AssistantChat.d.ts.map +1 -1
  65. package/dist/client/AssistantChat.js +28 -4
  66. package/dist/client/AssistantChat.js.map +1 -1
  67. package/dist/client/guided-questions.d.ts.map +1 -1
  68. package/dist/client/guided-questions.js +10 -6
  69. package/dist/client/guided-questions.js.map +1 -1
  70. package/dist/client/tool-display.d.ts.map +1 -1
  71. package/dist/client/tool-display.js +8 -0
  72. package/dist/client/tool-display.js.map +1 -1
  73. package/dist/collab/awareness.d.ts +2 -2
  74. package/dist/collab/awareness.d.ts.map +1 -1
  75. package/dist/collab/routes.d.ts +1 -1
  76. package/dist/file-upload/builder.d.ts.map +1 -1
  77. package/dist/file-upload/builder.js +6 -2
  78. package/dist/file-upload/builder.js.map +1 -1
  79. package/dist/notifications/channels.d.ts +10 -0
  80. package/dist/notifications/channels.d.ts.map +1 -1
  81. package/dist/notifications/channels.js +172 -20
  82. package/dist/notifications/channels.js.map +1 -1
  83. package/dist/notifications/routes.d.ts +3 -3
  84. package/dist/observability/routes.d.ts +5 -5
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/resources/handlers.d.ts +1 -1
  87. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  88. package/dist/server/analytics.d.ts +16 -0
  89. package/dist/server/analytics.d.ts.map +1 -1
  90. package/dist/server/analytics.js +30 -7
  91. package/dist/server/analytics.js.map +1 -1
  92. package/dist/server/ssr-handler.d.ts.map +1 -1
  93. package/dist/server/ssr-handler.js +28 -12
  94. package/dist/server/ssr-handler.js.map +1 -1
  95. package/dist/server/transcribe-voice.d.ts +1 -1
  96. 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
+ });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Analytics can now notify inbox, email, Slack, or webhook channels when first-party events spike.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Session replays now play back in production instead of showing a blank recording, because chunk downloads no longer rely on a manual gzip content-encoding that serverless hosts corrupted.
@@ -20,3 +20,6 @@ timeout = 75
20
20
  [functions.dashboard-report-sweep-background]
21
21
  timeout = 300
22
22
  memory = "2gb"
23
+
24
+ [functions.analytics-alert-sweep-background]
25
+ timeout = 120
@@ -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.data;
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
+ }