@agent-native/core 0.141.5 → 0.141.6
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/templates/analytics/.agents/skills/data-querying/SKILL.md +13 -6
- package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +15 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
- package/corpus/templates/analytics/app/i18n-data.ts +328 -0
- package/corpus/templates/analytics/app/pages/DataSources.tsx +66 -30
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-now-repair-automatically-after-.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +342 -0
- package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +8 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +11 -29
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +55 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +117 -31
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +15 -9
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +5 -16
- package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +61 -0
- package/corpus/templates/analytics/server/plugins/db.ts +58 -187
- package/corpus/templates/analytics/server/routes/api/analytics-rollups/backfill.post.ts +53 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-overlays-now-use-clean-surfaces-without-clipped-shadow.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -2
- package/corpus/templates/clips/desktop/src/styles.css +3 -54
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -3
- package/corpus/templates/design/actions/get-design-system.ts +3 -1
- package/corpus/templates/design/changelog/2026-08-05-missing-design-systems-now-return-a-clear-not-found-response.md +6 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +5 -5
- package/corpus/templates/slides/changelog/2026-08-05-new-presentation-prompts-now-clearly-describe-presentation-g.md +6 -0
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +10 -3
- package/dist/client/error-format.d.ts +1 -0
- package/dist/client/error-format.js +4 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +0 -6
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +0 -6
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SqlDashboardConfig } from "../../app/pages/adhoc/sql-dashboard/types";
|
|
2
2
|
import { loadDashboardSeed } from "./dashboard-seeds";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
listDashboardSummaries,
|
|
5
|
+
type DashboardRecord,
|
|
6
|
+
type DashboardSummaryRecord,
|
|
7
|
+
} from "./dashboards-store";
|
|
4
8
|
import {
|
|
5
9
|
FIRST_PARTY_DASHBOARD_ID,
|
|
6
10
|
repairFirstPartyObservedRetentionPanels,
|
|
@@ -1021,33 +1025,13 @@ export function cloneDashboardConfig(
|
|
|
1021
1025
|
return JSON.parse(JSON.stringify(entry.buildConfig())) as SqlDashboardConfig;
|
|
1022
1026
|
}
|
|
1023
1027
|
|
|
1024
|
-
function templateIdFromConfig(config: Record<string, unknown>): string | null {
|
|
1025
|
-
const catalog = config.catalog;
|
|
1026
|
-
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) {
|
|
1027
|
-
return null;
|
|
1028
|
-
}
|
|
1029
|
-
const templateId = (catalog as Record<string, unknown>).templateId;
|
|
1030
|
-
return typeof templateId === "string" && templateId ? templateId : null;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function demoIdFromConfig(config: Record<string, unknown>): string | null {
|
|
1034
|
-
const demo = config.demo;
|
|
1035
|
-
if (!demo || typeof demo !== "object" || Array.isArray(demo)) {
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
const demoId = (demo as Record<string, unknown>).id;
|
|
1039
|
-
return typeof demoId === "string" && demoId ? demoId : null;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1028
|
function installedDashboardForTemplate(
|
|
1043
|
-
row:
|
|
1029
|
+
row: DashboardSummaryRecord,
|
|
1044
1030
|
entry: DashboardCatalogEntry,
|
|
1045
1031
|
): boolean {
|
|
1046
|
-
const templateId = templateIdFromConfig(row.config);
|
|
1047
|
-
const demoId = demoIdFromConfig(row.config);
|
|
1048
1032
|
return (
|
|
1049
|
-
|
|
1050
|
-
demoId === entry.id ||
|
|
1033
|
+
row.catalogTemplateId === entry.id ||
|
|
1034
|
+
row.demoId === entry.id ||
|
|
1051
1035
|
row.id === entry.defaultDashboardId
|
|
1052
1036
|
);
|
|
1053
1037
|
}
|
|
@@ -1055,10 +1039,11 @@ function installedDashboardForTemplate(
|
|
|
1055
1039
|
export async function listDashboardCatalog(
|
|
1056
1040
|
ctx: AccessCtx,
|
|
1057
1041
|
): Promise<DashboardCatalogTemplate[]> {
|
|
1058
|
-
const dashboards = await
|
|
1042
|
+
const dashboards = await listDashboardSummaries(ctx, {
|
|
1059
1043
|
kind: "sql",
|
|
1060
1044
|
archived: "all",
|
|
1061
1045
|
hidden: "all",
|
|
1046
|
+
includeCatalogMetadata: true,
|
|
1062
1047
|
});
|
|
1063
1048
|
|
|
1064
1049
|
return dashboardCatalogEntries
|
|
@@ -1068,10 +1053,7 @@ export async function listDashboardCatalog(
|
|
|
1068
1053
|
.filter((row) => installedDashboardForTemplate(row, entry))
|
|
1069
1054
|
.map((row) => ({
|
|
1070
1055
|
id: row.id,
|
|
1071
|
-
name:
|
|
1072
|
-
typeof row.config.name === "string" && row.config.name.trim()
|
|
1073
|
-
? row.config.name
|
|
1074
|
-
: row.title,
|
|
1056
|
+
name: row.configName?.trim() || row.name,
|
|
1075
1057
|
visibility: row.visibility,
|
|
1076
1058
|
updatedAt: row.updatedAt,
|
|
1077
1059
|
archivedAt: row.archivedAt,
|
|
@@ -62,6 +62,9 @@ export interface DashboardSummaryRecord {
|
|
|
62
62
|
id: string;
|
|
63
63
|
kind: DashboardKind;
|
|
64
64
|
name: string;
|
|
65
|
+
configName: string | null;
|
|
66
|
+
catalogTemplateId: string | null;
|
|
67
|
+
demoId: string | null;
|
|
65
68
|
parentId: string | null;
|
|
66
69
|
ownerEmail: string;
|
|
67
70
|
orgId: string | null;
|
|
@@ -287,6 +290,24 @@ function configFromSettings(data: Record<string, unknown>): {
|
|
|
287
290
|
return { title, config: data };
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
function stringProperty(value: unknown, key: string): string | null {
|
|
294
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
295
|
+
const property = (value as Record<string, unknown>)[key];
|
|
296
|
+
return typeof property === "string" ? property : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function catalogMetadataFromConfig(config: Record<string, unknown>): {
|
|
300
|
+
configName: string | null;
|
|
301
|
+
catalogTemplateId: string | null;
|
|
302
|
+
demoId: string | null;
|
|
303
|
+
} {
|
|
304
|
+
return {
|
|
305
|
+
configName: typeof config.name === "string" ? config.name : null,
|
|
306
|
+
catalogTemplateId: stringProperty(config.catalog, "templateId"),
|
|
307
|
+
demoId: stringProperty(config.demo, "id"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
async function migrateDashboardFromSettings(
|
|
291
312
|
id: string,
|
|
292
313
|
kind: DashboardKind,
|
|
@@ -510,11 +531,13 @@ export async function listDashboardSummaries(
|
|
|
510
531
|
kind?: DashboardKind;
|
|
511
532
|
archived?: DashboardArchiveFilter;
|
|
512
533
|
hidden?: DashboardHiddenFilter;
|
|
534
|
+
includeCatalogMetadata?: boolean;
|
|
513
535
|
},
|
|
514
536
|
): Promise<DashboardSummaryRecord[]> {
|
|
515
537
|
const db = getDb() as any;
|
|
516
538
|
const archived = filter?.archived ?? "active";
|
|
517
539
|
const hidden = filter?.hidden ?? "visible";
|
|
540
|
+
const includeCatalogMetadata = filter?.includeCatalogMetadata === true;
|
|
518
541
|
const conditions: any[] = [
|
|
519
542
|
accessFilter(schema.dashboards, schema.dashboardShares, {
|
|
520
543
|
userEmail: ctx.email,
|
|
@@ -535,11 +558,31 @@ export async function listDashboardSummaries(
|
|
|
535
558
|
: sql<
|
|
536
559
|
string | null
|
|
537
560
|
>`json_extract(${schema.dashboards.config}, '$.parentId')`;
|
|
561
|
+
const configName = isPostgres()
|
|
562
|
+
? sql<string | null>`(${schema.dashboards.config}::jsonb ->> 'name')`
|
|
563
|
+
: sql<string | null>`json_extract(${schema.dashboards.config}, '$.name')`;
|
|
564
|
+
const catalogTemplateId = isPostgres()
|
|
565
|
+
? sql<
|
|
566
|
+
string | null
|
|
567
|
+
>`(${schema.dashboards.config}::jsonb -> 'catalog' ->> 'templateId')`
|
|
568
|
+
: sql<
|
|
569
|
+
string | null
|
|
570
|
+
>`json_extract(${schema.dashboards.config}, '$.catalog.templateId')`;
|
|
571
|
+
const demoId = isPostgres()
|
|
572
|
+
? sql<
|
|
573
|
+
string | null
|
|
574
|
+
>`(${schema.dashboards.config}::jsonb -> 'demo' ->> 'id')`
|
|
575
|
+
: sql<
|
|
576
|
+
string | null
|
|
577
|
+
>`json_extract(${schema.dashboards.config}, '$.demo.id')`;
|
|
538
578
|
const rows = await db
|
|
539
579
|
.select({
|
|
540
580
|
id: schema.dashboards.id,
|
|
541
581
|
kind: schema.dashboards.kind,
|
|
542
582
|
name: schema.dashboards.title,
|
|
583
|
+
...(includeCatalogMetadata
|
|
584
|
+
? { configName, catalogTemplateId, demoId }
|
|
585
|
+
: {}),
|
|
543
586
|
parentId,
|
|
544
587
|
ownerEmail: schema.dashboards.ownerEmail,
|
|
545
588
|
orgId: schema.dashboards.orgId,
|
|
@@ -554,6 +597,10 @@ export async function listDashboardSummaries(
|
|
|
554
597
|
.where(where);
|
|
555
598
|
const out: DashboardSummaryRecord[] = rows.map((row: any) => ({
|
|
556
599
|
...row,
|
|
600
|
+
configName: typeof row.configName === "string" ? row.configName : null,
|
|
601
|
+
catalogTemplateId:
|
|
602
|
+
typeof row.catalogTemplateId === "string" ? row.catalogTemplateId : null,
|
|
603
|
+
demoId: typeof row.demoId === "string" ? row.demoId : null,
|
|
557
604
|
parentId: typeof row.parentId === "string" ? row.parentId : null,
|
|
558
605
|
orgId: row.orgId ?? null,
|
|
559
606
|
archivedAt: row.archivedAt ?? null,
|
|
@@ -590,12 +637,20 @@ export async function listDashboardSummaries(
|
|
|
590
637
|
seen.add(id);
|
|
591
638
|
const config = value as Record<string, unknown>;
|
|
592
639
|
const { title } = configFromSettings(config);
|
|
640
|
+
const catalogMetadata = includeCatalogMetadata
|
|
641
|
+
? catalogMetadataFromConfig(config)
|
|
642
|
+
: {
|
|
643
|
+
configName: null,
|
|
644
|
+
catalogTemplateId: null,
|
|
645
|
+
demoId: null,
|
|
646
|
+
};
|
|
593
647
|
const createdAt =
|
|
594
648
|
typeof config.createdAt === "string" ? config.createdAt : nowIso();
|
|
595
649
|
out.push({
|
|
596
650
|
id,
|
|
597
651
|
kind,
|
|
598
652
|
name: title,
|
|
653
|
+
...catalogMetadata,
|
|
599
654
|
parentId: typeof config.parentId === "string" ? config.parentId : null,
|
|
600
655
|
ownerEmail: ctx.email,
|
|
601
656
|
orgId,
|
|
@@ -13,11 +13,33 @@ export const FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS = {
|
|
|
13
13
|
recommendMaxQueryMs: 30_000,
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const EXTERNAL_BACKEND_DEFINITIONS = [
|
|
17
|
+
{
|
|
18
|
+
id: "bigquery",
|
|
19
|
+
label: "BigQuery",
|
|
20
|
+
role: "warehouse",
|
|
21
|
+
requiredKeys: [
|
|
22
|
+
"GOOGLE_APPLICATION_CREDENTIALS_JSON",
|
|
23
|
+
"BIGQUERY_PROJECT_ID",
|
|
24
|
+
],
|
|
25
|
+
setupLink: buildDeepLink({
|
|
26
|
+
app: "analytics",
|
|
27
|
+
view: "data-sources",
|
|
28
|
+
to: "/data-sources?source=bigquery",
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "amplitude",
|
|
33
|
+
label: "Amplitude",
|
|
34
|
+
role: "product-analytics",
|
|
35
|
+
requiredKeys: ["AMPLITUDE_API_KEY", "AMPLITUDE_SECRET_KEY"],
|
|
36
|
+
setupLink: buildDeepLink({
|
|
37
|
+
app: "analytics",
|
|
38
|
+
view: "data-sources",
|
|
39
|
+
to: "/data-sources?source=amplitude",
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
] as const;
|
|
21
43
|
|
|
22
44
|
export type FirstPartyAnalyticsQueryClass =
|
|
23
45
|
| "raw-events"
|
|
@@ -39,9 +61,29 @@ export type FirstPartyAnalyticsRecommendation =
|
|
|
39
61
|
| "connect_bigquery"
|
|
40
62
|
| "use_bigquery";
|
|
41
63
|
|
|
64
|
+
export type FirstPartyAnalyticsExternalBackendRecommendation =
|
|
65
|
+
| "none"
|
|
66
|
+
| "connect"
|
|
67
|
+
| "use"
|
|
68
|
+
| "unknown";
|
|
69
|
+
|
|
70
|
+
export type FirstPartyAnalyticsBackendId =
|
|
71
|
+
(typeof EXTERNAL_BACKEND_DEFINITIONS)[number]["id"];
|
|
72
|
+
|
|
73
|
+
export interface FirstPartyAnalyticsBackendStatus {
|
|
74
|
+
id: FirstPartyAnalyticsBackendId;
|
|
75
|
+
label: string;
|
|
76
|
+
role: (typeof EXTERNAL_BACKEND_DEFINITIONS)[number]["role"];
|
|
77
|
+
configured: boolean | null;
|
|
78
|
+
missingRequiredKeys: string[];
|
|
79
|
+
setupLink: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
42
82
|
export interface FirstPartyAnalyticsHealth {
|
|
43
83
|
status: FirstPartyAnalyticsHealthStatus;
|
|
44
84
|
recommendation: FirstPartyAnalyticsRecommendation;
|
|
85
|
+
externalBackendRecommendation: FirstPartyAnalyticsExternalBackendRecommendation;
|
|
86
|
+
externalBackends: FirstPartyAnalyticsBackendStatus[];
|
|
45
87
|
reasons: Array<"event_volume" | "slow_queries" | "query_timeout">;
|
|
46
88
|
observedAt: string;
|
|
47
89
|
metrics: {
|
|
@@ -56,11 +98,8 @@ export interface FirstPartyAnalyticsHealth {
|
|
|
56
98
|
maxQueryDurationMs24h: number;
|
|
57
99
|
};
|
|
58
100
|
thresholds: typeof FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
missingRequiredKeys: string[];
|
|
62
|
-
setupLink: string;
|
|
63
|
-
};
|
|
101
|
+
/** Kept for clients that still read the original BigQuery-only field. */
|
|
102
|
+
bigQuery: FirstPartyAnalyticsBackendStatus;
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
interface QueryPressureEvent {
|
|
@@ -199,18 +238,13 @@ function dateSpanDays(
|
|
|
199
238
|
return Math.floor((last - first) / 86_400_000) + 1;
|
|
200
239
|
}
|
|
201
240
|
|
|
202
|
-
async function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}> {
|
|
207
|
-
const requiredKeys = [
|
|
208
|
-
"GOOGLE_APPLICATION_CREDENTIALS_JSON",
|
|
209
|
-
"BIGQUERY_PROJECT_ID",
|
|
210
|
-
];
|
|
241
|
+
async function externalBackendStatus(
|
|
242
|
+
scope: AnalyticsScope,
|
|
243
|
+
definition: (typeof EXTERNAL_BACKEND_DEFINITIONS)[number],
|
|
244
|
+
): Promise<FirstPartyAnalyticsBackendStatus> {
|
|
211
245
|
try {
|
|
212
246
|
const configured = await Promise.all(
|
|
213
|
-
requiredKeys.map((key) =>
|
|
247
|
+
definition.requiredKeys.map((key) =>
|
|
214
248
|
hasCredential(key, {
|
|
215
249
|
userEmail: scope.userEmail,
|
|
216
250
|
orgId: scope.orgId,
|
|
@@ -218,22 +252,41 @@ async function bigQueryStatus(scope: AnalyticsScope): Promise<{
|
|
|
218
252
|
),
|
|
219
253
|
);
|
|
220
254
|
return {
|
|
255
|
+
id: definition.id,
|
|
256
|
+
label: definition.label,
|
|
257
|
+
role: definition.role,
|
|
221
258
|
configured: configured.every(Boolean),
|
|
222
|
-
missingRequiredKeys: requiredKeys.filter(
|
|
259
|
+
missingRequiredKeys: definition.requiredKeys.filter(
|
|
223
260
|
(_, index) => !configured[index],
|
|
224
261
|
),
|
|
225
|
-
setupLink:
|
|
262
|
+
setupLink: definition.setupLink,
|
|
226
263
|
};
|
|
227
264
|
} catch (error) {
|
|
228
|
-
console.warn(
|
|
265
|
+
console.warn(
|
|
266
|
+
`[first-party-analytics] ${definition.label} status unavailable:`,
|
|
267
|
+
error,
|
|
268
|
+
);
|
|
229
269
|
return {
|
|
270
|
+
id: definition.id,
|
|
271
|
+
label: definition.label,
|
|
272
|
+
role: definition.role,
|
|
230
273
|
configured: null,
|
|
231
274
|
missingRequiredKeys: [],
|
|
232
|
-
setupLink:
|
|
275
|
+
setupLink: definition.setupLink,
|
|
233
276
|
};
|
|
234
277
|
}
|
|
235
278
|
}
|
|
236
279
|
|
|
280
|
+
async function externalBackendStatuses(
|
|
281
|
+
scope: AnalyticsScope,
|
|
282
|
+
): Promise<FirstPartyAnalyticsBackendStatus[]> {
|
|
283
|
+
return Promise.all(
|
|
284
|
+
EXTERNAL_BACKEND_DEFINITIONS.map((definition) =>
|
|
285
|
+
externalBackendStatus(scope, definition),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
237
290
|
function recommendationFor(
|
|
238
291
|
status: FirstPartyAnalyticsHealthStatus,
|
|
239
292
|
configured: boolean | null,
|
|
@@ -242,6 +295,18 @@ function recommendationFor(
|
|
|
242
295
|
return configured === true ? "use_bigquery" : "connect_bigquery";
|
|
243
296
|
}
|
|
244
297
|
|
|
298
|
+
function externalBackendRecommendationFor(
|
|
299
|
+
status: FirstPartyAnalyticsHealthStatus,
|
|
300
|
+
backends: FirstPartyAnalyticsBackendStatus[],
|
|
301
|
+
): FirstPartyAnalyticsExternalBackendRecommendation {
|
|
302
|
+
if (status !== "recommend_bigquery") return "none";
|
|
303
|
+
if (backends.some((backend) => backend.configured === true)) return "use";
|
|
304
|
+
if (backends.every((backend) => backend.configured === false)) {
|
|
305
|
+
return "connect";
|
|
306
|
+
}
|
|
307
|
+
return "unknown";
|
|
308
|
+
}
|
|
309
|
+
|
|
245
310
|
export async function getFirstPartyAnalyticsHealth(
|
|
246
311
|
scope: AnalyticsScope,
|
|
247
312
|
): Promise<FirstPartyAnalyticsHealth> {
|
|
@@ -256,7 +321,7 @@ export async function getFirstPartyAnalyticsHealth(
|
|
|
256
321
|
const pressure = schema.analyticsQueryPressureDaily;
|
|
257
322
|
const db = getDb() as any;
|
|
258
323
|
|
|
259
|
-
const [rollupRows, pressureRows,
|
|
324
|
+
const [rollupRows, pressureRows, externalBackends] = await Promise.all([
|
|
260
325
|
db
|
|
261
326
|
.select({
|
|
262
327
|
eventCount: sql<number>`coalesce(sum(${rollups.eventCount}), 0)`,
|
|
@@ -284,9 +349,14 @@ export async function getFirstPartyAnalyticsHealth(
|
|
|
284
349
|
lte(pressure.lastSeenAt, nowTimestamp),
|
|
285
350
|
),
|
|
286
351
|
),
|
|
287
|
-
|
|
352
|
+
externalBackendStatuses(scope),
|
|
288
353
|
]);
|
|
289
354
|
|
|
355
|
+
const bigQuery = externalBackends.find(
|
|
356
|
+
(backend) => backend.id === "bigquery",
|
|
357
|
+
);
|
|
358
|
+
if (!bigQuery) throw new Error("BigQuery backend status is missing");
|
|
359
|
+
|
|
290
360
|
const rollup = (rollupRows[0] ?? {}) as Record<string, unknown>;
|
|
291
361
|
const eventCount = finiteNumber(rollup.eventCount);
|
|
292
362
|
const dailyRollupRows = finiteNumber(rollup.dailyRollupRows);
|
|
@@ -348,6 +418,11 @@ export async function getFirstPartyAnalyticsHealth(
|
|
|
348
418
|
return {
|
|
349
419
|
status,
|
|
350
420
|
recommendation: recommendationFor(status, bigQuery.configured),
|
|
421
|
+
externalBackendRecommendation: externalBackendRecommendationFor(
|
|
422
|
+
status,
|
|
423
|
+
externalBackends,
|
|
424
|
+
),
|
|
425
|
+
externalBackends,
|
|
351
426
|
reasons,
|
|
352
427
|
observedAt: nowIso(),
|
|
353
428
|
metrics: {
|
|
@@ -367,9 +442,24 @@ export async function getFirstPartyAnalyticsHealth(
|
|
|
367
442
|
}
|
|
368
443
|
|
|
369
444
|
export function unavailableFirstPartyAnalyticsHealth(): FirstPartyAnalyticsHealth {
|
|
445
|
+
const externalBackends = EXTERNAL_BACKEND_DEFINITIONS.map((definition) => ({
|
|
446
|
+
id: definition.id,
|
|
447
|
+
label: definition.label,
|
|
448
|
+
role: definition.role,
|
|
449
|
+
configured: null,
|
|
450
|
+
missingRequiredKeys: [],
|
|
451
|
+
setupLink: definition.setupLink,
|
|
452
|
+
}));
|
|
453
|
+
const bigQuery = externalBackends.find(
|
|
454
|
+
(backend) => backend.id === "bigquery",
|
|
455
|
+
);
|
|
456
|
+
if (!bigQuery) throw new Error("BigQuery backend status is missing");
|
|
457
|
+
|
|
370
458
|
return {
|
|
371
459
|
status: "unavailable",
|
|
372
460
|
recommendation: "none",
|
|
461
|
+
externalBackendRecommendation: "unknown",
|
|
462
|
+
externalBackends,
|
|
373
463
|
reasons: [],
|
|
374
464
|
observedAt: nowIso(),
|
|
375
465
|
metrics: {
|
|
@@ -384,10 +474,6 @@ export function unavailableFirstPartyAnalyticsHealth(): FirstPartyAnalyticsHealt
|
|
|
384
474
|
maxQueryDurationMs24h: 0,
|
|
385
475
|
},
|
|
386
476
|
thresholds: FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS,
|
|
387
|
-
bigQuery
|
|
388
|
-
configured: null,
|
|
389
|
-
missingRequiredKeys: [],
|
|
390
|
-
setupLink: BIGQUERY_SETUP_LINK,
|
|
391
|
-
},
|
|
477
|
+
bigQuery,
|
|
392
478
|
};
|
|
393
479
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { isPostgres } from "@agent-native/core/db";
|
|
1
2
|
import { sql } from "@agent-native/core/db/schema";
|
|
2
3
|
|
|
3
4
|
import { getDb, schema } from "../db/index.js";
|
|
5
|
+
import {
|
|
6
|
+
FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY,
|
|
7
|
+
FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
|
|
8
|
+
} from "./analytics-rollup-lock.js";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* The normalized subset emitted by first-party analytics ingest. Keeping this
|
|
@@ -23,15 +28,6 @@ export interface FirstPartyAnalyticsRollupResult {
|
|
|
23
28
|
userDayCount: number;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
/**
|
|
27
|
-
* Shared with the historical backfill so live ingest and migration work hold
|
|
28
|
-
* the same transaction-scoped Postgres lock while they update rollups.
|
|
29
|
-
*/
|
|
30
|
-
export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY =
|
|
31
|
-
"agent-native:analytics-rollup-backfill";
|
|
32
|
-
export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL =
|
|
33
|
-
"SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))";
|
|
34
|
-
|
|
35
31
|
interface DailyRollupRow {
|
|
36
32
|
id: string;
|
|
37
33
|
tenantKey: string;
|
|
@@ -150,6 +146,16 @@ export async function upsertFirstPartyAnalyticsRollups(
|
|
|
150
146
|
}
|
|
151
147
|
|
|
152
148
|
const writeRollups = async (tx: any) => {
|
|
149
|
+
if (isPostgres()) {
|
|
150
|
+
// The historical backfill takes this lock before its raw-event
|
|
151
|
+
// snapshot. Holding it through ingest's rollup increment prevents the
|
|
152
|
+
// backfill's monotonic upsert from losing a concurrently committed
|
|
153
|
+
// event.
|
|
154
|
+
await tx.execute({
|
|
155
|
+
sql: FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
|
|
156
|
+
args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
153
159
|
const dailyRows = [...dailyRollups.values()];
|
|
154
160
|
await tx
|
|
155
161
|
.insert(schema.analyticsEventDailyRollups)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDbExec
|
|
1
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
2
2
|
import { and, eq, isNull, or } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
|
|
@@ -17,11 +17,7 @@ import {
|
|
|
17
17
|
queryOutcomeFromError,
|
|
18
18
|
recordFirstPartyAnalyticsQueryPressure,
|
|
19
19
|
} from "./first-party-analytics-health.js";
|
|
20
|
-
import {
|
|
21
|
-
FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY,
|
|
22
|
-
FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
|
|
23
|
-
upsertFirstPartyAnalyticsRollups,
|
|
24
|
-
} from "./first-party-analytics-rollups.js";
|
|
20
|
+
import { upsertFirstPartyAnalyticsRollups } from "./first-party-analytics-rollups.js";
|
|
25
21
|
|
|
26
22
|
export interface AnalyticsScope {
|
|
27
23
|
userEmail: string;
|
|
@@ -510,16 +506,9 @@ export async function recordAnalyticsEvents(
|
|
|
510
506
|
|
|
511
507
|
if (rows.length) {
|
|
512
508
|
await db.transaction(async (tx: any) => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// backfill's monotonic upsert from losing a concurrently committed
|
|
517
|
-
// event.
|
|
518
|
-
await tx.execute({
|
|
519
|
-
sql: FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
|
|
520
|
-
args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
|
|
521
|
-
});
|
|
522
|
-
}
|
|
509
|
+
// Raw events and incremental rollups share this transaction. The
|
|
510
|
+
// historical boot backfill is disabled, so no global cross-request lock
|
|
511
|
+
// is needed to coordinate a second writer.
|
|
523
512
|
await tx.insert(schema.analyticsEvents).values(rows);
|
|
524
513
|
await tx
|
|
525
514
|
.update(schema.analyticsPublicKeys)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { runAnalyticsRollupBackfillOnce } from "../jobs/analytics-rollup-backfill";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_INTERVAL_MS = 15 * 60 * 1000;
|
|
4
|
+
let skippingLogged = false;
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
var __AGENT_NATIVE_ANALYTICS_ROLLUP_BACKFILL_SCHEDULED_RUNTIME__:
|
|
8
|
+
| boolean
|
|
9
|
+
| undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function platformSchedulerOwnsBackfill(): boolean {
|
|
13
|
+
return (
|
|
14
|
+
process.env.NETLIFY === "true" ||
|
|
15
|
+
Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
|
|
16
|
+
globalThis.__AGENT_NATIVE_ANALYTICS_ROLLUP_BACKFILL_SCHEDULED_RUNTIME__ ===
|
|
17
|
+
true
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function intervalMs(): number {
|
|
22
|
+
const raw = process.env.ANALYTICS_ROLLUP_BACKFILL_INTERVAL_MS?.trim();
|
|
23
|
+
if (!raw) return DEFAULT_INTERVAL_MS;
|
|
24
|
+
const parsed = Number.parseInt(raw, 10);
|
|
25
|
+
return Number.isFinite(parsed) && parsed >= 10_000
|
|
26
|
+
? parsed
|
|
27
|
+
: DEFAULT_INTERVAL_MS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function registerAnalyticsRollupBackfillJobs(): void {
|
|
31
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
32
|
+
const flag =
|
|
33
|
+
process.env.ANALYTICS_ROLLUP_BACKFILL_JOBS ??
|
|
34
|
+
process.env.RUN_BACKGROUND_JOBS;
|
|
35
|
+
const enabled =
|
|
36
|
+
!platformSchedulerOwnsBackfill() &&
|
|
37
|
+
(flag === "1" || (isProd && flag !== "0"));
|
|
38
|
+
|
|
39
|
+
if (!enabled) {
|
|
40
|
+
if (!skippingLogged) {
|
|
41
|
+
console.log(
|
|
42
|
+
platformSchedulerOwnsBackfill()
|
|
43
|
+
? "[analytics-rollups] Skipping in-process cron because the platform scheduler owns historical rollup backfills."
|
|
44
|
+
: "[analytics-rollups] Skipping historical rollup backfill (set ANALYTICS_ROLLUP_BACKFILL_JOBS=1 or RUN_BACKGROUND_JOBS=1 to enable in dev; on by default in production)",
|
|
45
|
+
);
|
|
46
|
+
skippingLogged = true;
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ms = intervalMs();
|
|
52
|
+
setInterval(() => {
|
|
53
|
+
runAnalyticsRollupBackfillOnce().catch((err) =>
|
|
54
|
+
console.error("[analytics-rollups] historical backfill failed:", err),
|
|
55
|
+
);
|
|
56
|
+
}, ms);
|
|
57
|
+
|
|
58
|
+
console.log(
|
|
59
|
+
`[analytics-rollups] Historical rollup backfill scheduled every ${ms / 1000}s.`,
|
|
60
|
+
);
|
|
61
|
+
}
|