@agent-native/core 0.147.0 → 0.148.0
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/frontend-design/SKILL.md +115 -68
- package/corpus/templates/analytics/DEVELOPING.md +50 -3
- package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +190 -0
- package/corpus/templates/analytics/app/i18n/index.ts +14 -29
- package/corpus/templates/analytics/app/i18n-data.ts +10 -10
- package/corpus/templates/analytics/app/pages/Settings.tsx +5 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -3
- package/corpus/templates/analytics/scripts/first-party-analytics-bigquery-backfill.ts +197 -18
- package/corpus/templates/analytics/server/db/schema.ts +23 -0
- package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +3 -1
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +1 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +113 -13
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +140 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-volume.ts +207 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +40 -9
- package/corpus/templates/analytics/server/lib/gcloud.ts +148 -20
- package/corpus/templates/analytics/server/plugins/db.ts +42 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/assets/app/i18n/index.ts +14 -29
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/brain/app/i18n/index.ts +14 -29
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/chat/app/i18n/index.ts +14 -29
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -7
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +18 -13
- package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +87 -370
- package/corpus/templates/clips/app/components/settings/slack-section.tsx +59 -101
- package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +136 -213
- package/corpus/templates/clips/app/i18n/en-US.ts +11 -8
- package/corpus/templates/clips/desktop/src/app.tsx +1 -1
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/content/app/i18n/index.ts +14 -29
- package/corpus/templates/crm/app/i18n/index.ts +14 -29
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/design/actions/analyze-brand-assets.ts +15 -15
- package/corpus/templates/design/actions/import-code.ts +2 -43
- package/corpus/templates/design/app/i18n/index.ts +14 -29
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/dispatch/app/i18n-data.ts +1 -1
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/factory/AGENTS.md +40 -49
- package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +66 -0
- package/corpus/templates/factory/actions/evaluate-triage-item.ts +37 -0
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -0
- package/corpus/templates/factory/actions/get-triage-item.ts +25 -4
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +55 -0
- package/corpus/templates/factory/actions/ingest-github-observation.ts +21 -0
- package/corpus/templates/factory/actions/list-factory-audit.ts +118 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
- package/corpus/templates/factory/actions/list-triage-items.ts +24 -2
- package/corpus/templates/factory/actions/poll-github-sources.ts +60 -0
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +39 -0
- package/corpus/templates/factory/actions/poll-slack-channel.ts +45 -1
- package/corpus/templates/factory/actions/run-factory-automation.ts +1 -1
- package/corpus/templates/factory/actions/save-factory-automation.ts +2 -2
- package/corpus/templates/factory/actions/start-builder-for-item.ts +51 -0
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +685 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +6 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +25 -0
- package/corpus/templates/factory/app/routes/factory.tsx +95 -42
- package/corpus/templates/factory/server/db/schema.ts +37 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +5 -1
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +29 -0
- package/corpus/templates/factory/server/triage/audit.ts +79 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/forms/app/i18n/index.ts +14 -29
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/macros/app/i18n/index.ts +14 -29
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/mail/app/i18n/index.ts +14 -29
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +7 -7
- package/corpus/templates/plan/app/i18n/index.ts +14 -29
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/slides/actions/analyze-brand-assets.ts +15 -15
- package/corpus/templates/slides/actions/import-code.ts +2 -43
- package/corpus/templates/slides/app/i18n/index.ts +14 -29
- package/corpus/templates/slides/server/routes/api/preview-images.get.ts +4 -1
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/tasks/app/components/layout/HeaderActions.tsx +1 -84
- package/corpus/templates/tasks/app/hooks/use-mobile.tsx +1 -21
- package/corpus/templates/tasks/app/i18n/index.ts +14 -29
- package/dist/agent/engine/ai-sdk-engine.js +2 -1
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +2 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +5 -1
- package/dist/agent/engine/provider-endpoint-validation.d.ts +9 -0
- package/dist/agent/engine/provider-endpoint-validation.js +26 -0
- package/dist/agent/engine/provider-env-vars.d.ts +1 -1
- package/dist/agent/engine/provider-env-vars.js +1 -1
- package/dist/agent/engine/registry.d.ts +8 -6
- package/dist/agent/engine/registry.js +37 -24
- package/dist/agent/tool-search.d.ts +1 -0
- package/dist/agent/tool-search.js +17 -1
- package/dist/automations/service.js +4 -10
- package/dist/brand-kit/brand-signals.d.ts +13 -0
- package/dist/brand-kit/brand-signals.js +15 -0
- package/dist/brand-kit/index.d.ts +2 -2
- package/dist/brand-kit/index.js +1 -1
- package/dist/cli/plan-blocks.d.ts +3 -18
- package/dist/cli/plan-blocks.js +2 -95
- package/dist/cli/plan-publish-store.d.ts +2 -61
- package/dist/cli/plan-publish-store.js +2 -127
- package/dist/cli/skills-content/connection.d.ts +1 -1
- package/dist/cli/skills-content/connection.js +1 -52
- package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-recap-skill.js +1 -553
- package/dist/client/agent-engine-key.d.ts +14 -4
- package/dist/client/agent-engine-key.js +51 -5
- package/dist/client/agent-provider-catalog.d.ts +20 -0
- package/dist/client/agent-provider-catalog.js +100 -0
- package/dist/client/blocks/index.d.ts +1 -0
- package/dist/client/blocks/index.js +1 -0
- package/dist/client/chat/run-recovery.js +4 -48
- package/dist/client/chat-first.js +4 -3
- package/dist/client/i18n.d.ts +14 -0
- package/dist/client/i18n.js +18 -0
- package/dist/client/settings/ProviderSetupForm.d.ts +21 -0
- package/dist/client/settings/ProviderSetupForm.js +145 -0
- package/dist/client/settings/SettingsPanel.js +52 -49
- package/dist/client/settings/index.d.ts +2 -0
- package/dist/client/settings/index.js +2 -0
- package/dist/coding-tools/run-code.d.ts +5 -0
- package/dist/coding-tools/run-code.js +196 -36
- package/dist/coding-tools/tool-orchestration.d.ts +10 -0
- package/dist/coding-tools/tool-orchestration.js +107 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/jobs/frontmatter.d.ts +9 -0
- package/dist/jobs/frontmatter.js +16 -0
- package/dist/jobs/scheduler.js +8 -8
- package/dist/jobs/tools.d.ts +1 -1
- package/dist/jobs/tools.js +2 -7
- package/dist/localization/default-messages.d.ts +20 -0
- package/dist/localization/default-messages.js +25 -5
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.js +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +34 -10
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/run-code-tools.d.ts +1 -1
- package/dist/server/agent-chat/run-code-tools.js +3 -1
- package/dist/server/agent-chat-plugin.js +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.js +43 -12
- package/dist/server/agents-bundle.js +2 -1
- package/dist/server/auth.js +21 -5
- package/dist/server/credential-provider.js +1 -0
- package/dist/server/design-token-utils.d.ts +18 -0
- package/dist/server/design-token-utils.js +31 -0
- package/dist/server/onboarding-html.js +22 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
- package/dist/templates/chat/app/i18n/index.ts +14 -29
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
- package/dist/templates/default/app/i18n/index.ts +14 -29
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
- package/dist/triggers/dispatcher.js +1 -8
- package/docs/content/environment-variables.mdx +39 -38
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +115 -68
- package/src/templates/chat/app/i18n/index.ts +14 -29
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +115 -68
- package/src/templates/default/app/i18n/index.ts +14 -29
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +22 -9
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +115 -68
- package/corpus/templates/plan/app/components/plan/wireframe/sanitize-html.ts +0 -343
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type BigQueryTableRef,
|
|
7
7
|
} from "./bigquery.js";
|
|
8
8
|
import { requireRequestCredentialContext } from "./credentials-context.js";
|
|
9
|
-
import { getAccessToken } from "./gcloud.js";
|
|
9
|
+
import { fetchGoogleWithRetry, getAccessToken } from "./gcloud.js";
|
|
10
10
|
import {
|
|
11
11
|
getScopedSettingRecord,
|
|
12
12
|
putScopedSettingRecord,
|
|
@@ -78,6 +78,19 @@ const MAX_INSERT_BATCH_SIZE = 200;
|
|
|
78
78
|
const MAX_DEDICATED_INSERT_BATCH_SIZE = 500;
|
|
79
79
|
const MAX_DEDICATED_INSERT_CONCURRENCY = 4;
|
|
80
80
|
const MAX_INSERT_REQUEST_BYTES = 8 * 1024 * 1024;
|
|
81
|
+
const DEFAULT_BACKFILL_LOOKBACK_DAYS = 60;
|
|
82
|
+
const MIN_BACKFILL_LOOKBACK_DAYS = 30;
|
|
83
|
+
const MAX_BACKFILL_LOOKBACK_DAYS = 60;
|
|
84
|
+
const DEFAULT_BACKFILL_SKIP_EVENTS = ["http.response"];
|
|
85
|
+
const MAX_BACKFILL_SKIP_EVENTS = 32;
|
|
86
|
+
const BACKFILL_LOOKBACK_DAYS_ENV = "ANALYTICS_BIGQUERY_BACKFILL_LOOKBACK_DAYS";
|
|
87
|
+
const BACKFILL_SKIP_EVENTS_ENV = "ANALYTICS_BIGQUERY_BACKFILL_SKIP_EVENTS";
|
|
88
|
+
|
|
89
|
+
export interface FirstPartyAnalyticsBackfillOptions {
|
|
90
|
+
lookbackDays?: number;
|
|
91
|
+
skipEventNames?: string[];
|
|
92
|
+
now?: () => string;
|
|
93
|
+
}
|
|
81
94
|
|
|
82
95
|
export interface FirstPartyAnalyticsInsertOptions {
|
|
83
96
|
/** Maximum rows in one BigQuery insertAll request. */
|
|
@@ -319,7 +332,7 @@ async function insertBatch(
|
|
|
319
332
|
token: string,
|
|
320
333
|
rows: Record<string, unknown>[],
|
|
321
334
|
): Promise<void> {
|
|
322
|
-
const response = await
|
|
335
|
+
const response = await fetchGoogleWithRetry(
|
|
323
336
|
`https://bigquery.googleapis.com/bigquery/v2/projects/${table.projectId}/datasets/${table.datasetId}/tables/${table.tableId}/insertAll`,
|
|
324
337
|
{
|
|
325
338
|
method: "POST",
|
|
@@ -336,6 +349,7 @@ async function insertBatch(
|
|
|
336
349
|
})),
|
|
337
350
|
}),
|
|
338
351
|
},
|
|
352
|
+
"BigQuery insertAll",
|
|
339
353
|
);
|
|
340
354
|
if (!response.ok) {
|
|
341
355
|
const text = await response.text();
|
|
@@ -939,6 +953,7 @@ export async function queryFirstPartyAnalyticsInBigQuery(
|
|
|
939
953
|
|
|
940
954
|
export async function assertFirstPartyAnalyticsBigQueryReady(
|
|
941
955
|
configuredTable?: string | null,
|
|
956
|
+
options: { includeRowCount?: boolean } = {},
|
|
942
957
|
): Promise<{ table: BigQueryTableRef; rowCount: number }> {
|
|
943
958
|
const table = await getFirstPartyAnalyticsTable(configuredTable);
|
|
944
959
|
const expectedColumns = new Map<string, readonly string[]>([
|
|
@@ -1017,6 +1032,9 @@ export async function assertFirstPartyAnalyticsBigQueryReady(
|
|
|
1017
1032
|
missingColumns.join(", "),
|
|
1018
1033
|
);
|
|
1019
1034
|
}
|
|
1035
|
+
if (options.includeRowCount === false) {
|
|
1036
|
+
return { table, rowCount: 0 };
|
|
1037
|
+
}
|
|
1020
1038
|
const result = await runQuery(
|
|
1021
1039
|
"SELECT COUNT(*) AS row_count FROM " + quote + table.fullyQualified + quote,
|
|
1022
1040
|
);
|
|
@@ -1027,6 +1045,7 @@ export async function assertFirstPartyAnalyticsBigQueryReady(
|
|
|
1027
1045
|
export async function getFirstPartyAnalyticsBigQueryMetrics(
|
|
1028
1046
|
scope: FirstPartyAnalyticsScope,
|
|
1029
1047
|
configuredTable?: string | null,
|
|
1048
|
+
options: { includeLegacyOwnerRows?: boolean; startDate?: string } = {},
|
|
1030
1049
|
): Promise<{
|
|
1031
1050
|
eventCount: number;
|
|
1032
1051
|
dailyRollupRows: number;
|
|
@@ -1037,14 +1056,20 @@ export async function getFirstPartyAnalyticsBigQueryMetrics(
|
|
|
1037
1056
|
const physical = firstPartyAnalyticsPhysicalTables(table);
|
|
1038
1057
|
const ownerEmail = sqlLiteral(scope.userEmail);
|
|
1039
1058
|
const today = sqlLiteral(new Date().toISOString().slice(0, 10));
|
|
1059
|
+
const includeLegacyOwnerRows = options.includeLegacyOwnerRows !== false;
|
|
1040
1060
|
const tenantFilter = scope.orgId
|
|
1041
|
-
?
|
|
1061
|
+
? includeLegacyOwnerRows
|
|
1062
|
+
? `(org_id = ${sqlLiteral(scope.orgId)} OR (org_id IS NULL AND owner_email = ${ownerEmail}))`
|
|
1063
|
+
: `(org_id = ${sqlLiteral(scope.orgId)})`
|
|
1042
1064
|
: `(org_id IS NULL AND owner_email = ${ownerEmail})`;
|
|
1065
|
+
const startDateFilter = options.startDate
|
|
1066
|
+
? ` AND event_date >= ${sqlLiteral(options.startDate)}`
|
|
1067
|
+
: "";
|
|
1043
1068
|
const result = await runQuery(`
|
|
1044
1069
|
WITH scoped_events AS (
|
|
1045
1070
|
SELECT *
|
|
1046
1071
|
FROM \`${physical.events}\`
|
|
1047
|
-
WHERE ${tenantFilter} AND event_date <= ${today}
|
|
1072
|
+
WHERE ${tenantFilter}${startDateFilter} AND event_date <= ${today}
|
|
1048
1073
|
)
|
|
1049
1074
|
SELECT
|
|
1050
1075
|
COUNT(*) AS event_count,
|
|
@@ -1121,26 +1146,92 @@ function serializeBackfillCursor(
|
|
|
1121
1146
|
return JSON.stringify(cursor);
|
|
1122
1147
|
}
|
|
1123
1148
|
|
|
1149
|
+
function boundedLookbackDays(value: number): number {
|
|
1150
|
+
if (
|
|
1151
|
+
!Number.isInteger(value) ||
|
|
1152
|
+
value < MIN_BACKFILL_LOOKBACK_DAYS ||
|
|
1153
|
+
value > MAX_BACKFILL_LOOKBACK_DAYS
|
|
1154
|
+
) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
`BigQuery backfill lookback must be an integer between ${MIN_BACKFILL_LOOKBACK_DAYS} and ${MAX_BACKFILL_LOOKBACK_DAYS} days`,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
return value;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function configuredLookbackDays(requested: number | undefined): number {
|
|
1163
|
+
if (requested !== undefined) return boundedLookbackDays(requested);
|
|
1164
|
+
const raw = process.env[BACKFILL_LOOKBACK_DAYS_ENV]?.trim();
|
|
1165
|
+
if (!raw) return DEFAULT_BACKFILL_LOOKBACK_DAYS;
|
|
1166
|
+
const parsed = Number(raw);
|
|
1167
|
+
return Number.isInteger(parsed) &&
|
|
1168
|
+
parsed >= MIN_BACKFILL_LOOKBACK_DAYS &&
|
|
1169
|
+
parsed <= MAX_BACKFILL_LOOKBACK_DAYS
|
|
1170
|
+
? parsed
|
|
1171
|
+
: DEFAULT_BACKFILL_LOOKBACK_DAYS;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function configuredSkipEventNames(requested: string[] | undefined): string[] {
|
|
1175
|
+
const raw = requested
|
|
1176
|
+
? requested
|
|
1177
|
+
: (
|
|
1178
|
+
process.env[BACKFILL_SKIP_EVENTS_ENV] ??
|
|
1179
|
+
DEFAULT_BACKFILL_SKIP_EVENTS.join(",")
|
|
1180
|
+
)
|
|
1181
|
+
.split(",")
|
|
1182
|
+
.map((name) => name.trim());
|
|
1183
|
+
const names = [...new Set(raw)].filter(Boolean);
|
|
1184
|
+
if (names.length > MAX_BACKFILL_SKIP_EVENTS) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`BigQuery backfill skips at most ${MAX_BACKFILL_SKIP_EVENTS} event names`,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
return names;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function backfillLookbackStart(now: string, lookbackDays: number): string {
|
|
1193
|
+
const date = new Date(now);
|
|
1194
|
+
if (Number.isNaN(date.getTime())) {
|
|
1195
|
+
throw new Error(`Invalid BigQuery backfill clock value: ${now}`);
|
|
1196
|
+
}
|
|
1197
|
+
date.setUTCDate(date.getUTCDate() - lookbackDays);
|
|
1198
|
+
return date.toISOString();
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1124
1201
|
function backfillBranchSql(
|
|
1125
1202
|
predicate: string,
|
|
1126
1203
|
predicateArgs: string[],
|
|
1127
1204
|
cursor: FirstPartyAnalyticsBackfillCursor,
|
|
1205
|
+
lookbackStart: string,
|
|
1206
|
+
skipEventNames: string[],
|
|
1128
1207
|
): {
|
|
1129
1208
|
sql: string;
|
|
1130
|
-
args:
|
|
1209
|
+
args: unknown[];
|
|
1131
1210
|
} {
|
|
1132
|
-
const cursorSql = cursor.receivedAt
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1211
|
+
const cursorSql = cursor.receivedAt ? "(received_at, id) > (?, ?)" : "";
|
|
1212
|
+
const cursorArgs = cursor.receivedAt ? [cursor.receivedAt, cursor.id] : [];
|
|
1213
|
+
const skipSql =
|
|
1214
|
+
skipEventNames.length === DEFAULT_BACKFILL_SKIP_EVENTS.length &&
|
|
1215
|
+
skipEventNames[0] === DEFAULT_BACKFILL_SKIP_EVENTS[0]
|
|
1216
|
+
? "event_name IS DISTINCT FROM 'http.response'"
|
|
1217
|
+
: skipEventNames.length
|
|
1218
|
+
? `(event_name IS NULL OR event_name NOT IN (${skipEventNames.map(() => "?").join(", ")}))`
|
|
1219
|
+
: "";
|
|
1220
|
+
const filters = ["received_at >= ?", skipSql].filter(Boolean);
|
|
1138
1221
|
return {
|
|
1139
1222
|
sql: `SELECT id, received_at
|
|
1140
1223
|
FROM analytics_events
|
|
1141
|
-
WHERE ${predicate}
|
|
1224
|
+
WHERE ${predicate}
|
|
1225
|
+
AND ${filters.join("\n AND ")}${cursorSql ? `\n AND ${cursorSql}` : ""}
|
|
1142
1226
|
ORDER BY received_at ASC, id ASC LIMIT ?`,
|
|
1143
|
-
args: [
|
|
1227
|
+
args: [
|
|
1228
|
+
...predicateArgs,
|
|
1229
|
+
lookbackStart,
|
|
1230
|
+
...(skipSql === "event_name IS DISTINCT FROM 'http.response'"
|
|
1231
|
+
? []
|
|
1232
|
+
: skipEventNames),
|
|
1233
|
+
...cursorArgs,
|
|
1234
|
+
],
|
|
1144
1235
|
};
|
|
1145
1236
|
}
|
|
1146
1237
|
|
|
@@ -1191,6 +1282,7 @@ export async function backfillFirstPartyAnalyticsBatch(
|
|
|
1191
1282
|
cursor: string | null,
|
|
1192
1283
|
limit: number,
|
|
1193
1284
|
configuredTable?: string | null,
|
|
1285
|
+
options?: FirstPartyAnalyticsBackfillOptions,
|
|
1194
1286
|
): Promise<FirstPartyAnalyticsBackfillBatch> {
|
|
1195
1287
|
const boundedLimit = Math.min(
|
|
1196
1288
|
Math.max(Math.floor(limit), 1),
|
|
@@ -1198,6 +1290,12 @@ export async function backfillFirstPartyAnalyticsBatch(
|
|
|
1198
1290
|
);
|
|
1199
1291
|
const db = getDbExec();
|
|
1200
1292
|
const parsedCursor = parseBackfillCursor(cursor);
|
|
1293
|
+
const lookbackDays = configuredLookbackDays(options?.lookbackDays);
|
|
1294
|
+
const lookbackStart = backfillLookbackStart(
|
|
1295
|
+
options?.now?.() ?? new Date().toISOString(),
|
|
1296
|
+
lookbackDays,
|
|
1297
|
+
);
|
|
1298
|
+
const skipEventNames = configuredSkipEventNames(options?.skipEventNames);
|
|
1201
1299
|
const branches = scope.orgId
|
|
1202
1300
|
? [
|
|
1203
1301
|
{ predicate: "org_id = ?", args: [scope.orgId] },
|
|
@@ -1218,6 +1316,8 @@ export async function backfillFirstPartyAnalyticsBatch(
|
|
|
1218
1316
|
branch.predicate,
|
|
1219
1317
|
branch.args,
|
|
1220
1318
|
parsedCursor,
|
|
1319
|
+
lookbackStart,
|
|
1320
|
+
skipEventNames,
|
|
1221
1321
|
);
|
|
1222
1322
|
const result = await db.execute({
|
|
1223
1323
|
sql: scoped.sql,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { and, count, eq, gte, isNull, or } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { getDb, schema } from "../db/index.js";
|
|
4
|
+
|
|
5
|
+
export interface FirstPartyAnalyticsPurgeScope {
|
|
6
|
+
userEmail: string;
|
|
7
|
+
orgId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FirstPartyAnalyticsPostgresPurgeCounts {
|
|
11
|
+
eventRows: number;
|
|
12
|
+
dailyRollupRows: number;
|
|
13
|
+
userDayRows: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FirstPartyAnalyticsPurgeWindow {
|
|
17
|
+
startReceivedAt: string;
|
|
18
|
+
startEventDate: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scopePredicate(
|
|
22
|
+
table: {
|
|
23
|
+
orgId: any;
|
|
24
|
+
ownerEmail: any;
|
|
25
|
+
},
|
|
26
|
+
scope: FirstPartyAnalyticsPurgeScope,
|
|
27
|
+
includeLegacyOwnerRows: boolean,
|
|
28
|
+
) {
|
|
29
|
+
if (!includeLegacyOwnerRows) return eq(table.orgId, scope.orgId);
|
|
30
|
+
return or(
|
|
31
|
+
eq(table.orgId, scope.orgId),
|
|
32
|
+
and(isNull(table.orgId), eq(table.ownerEmail, scope.userEmail)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function windowPredicate(
|
|
37
|
+
table: {
|
|
38
|
+
eventDate: any;
|
|
39
|
+
receivedAt?: any;
|
|
40
|
+
},
|
|
41
|
+
predicate: any,
|
|
42
|
+
window: FirstPartyAnalyticsPurgeWindow,
|
|
43
|
+
) {
|
|
44
|
+
return table.receivedAt
|
|
45
|
+
? and(predicate, gte(table.receivedAt, window.startReceivedAt))
|
|
46
|
+
: and(predicate, gte(table.eventDate, window.startEventDate));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function countScopedRows(table: any, predicate: any): Promise<number> {
|
|
50
|
+
const [row] = await (getDb() as any)
|
|
51
|
+
.select({ count: count() })
|
|
52
|
+
.from(table)
|
|
53
|
+
.where(predicate);
|
|
54
|
+
const value = Number(row?.count ?? 0);
|
|
55
|
+
return Number.isFinite(value) ? value : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function countFirstPartyAnalyticsPostgresRows(
|
|
59
|
+
scope: FirstPartyAnalyticsPurgeScope,
|
|
60
|
+
includeLegacyOwnerRows: boolean,
|
|
61
|
+
window: FirstPartyAnalyticsPurgeWindow,
|
|
62
|
+
): Promise<FirstPartyAnalyticsPostgresPurgeCounts> {
|
|
63
|
+
const predicates = [
|
|
64
|
+
windowPredicate(
|
|
65
|
+
schema.analyticsEvents,
|
|
66
|
+
scopePredicate(schema.analyticsEvents, scope, includeLegacyOwnerRows),
|
|
67
|
+
window,
|
|
68
|
+
),
|
|
69
|
+
windowPredicate(
|
|
70
|
+
schema.analyticsEventDailyRollups,
|
|
71
|
+
scopePredicate(
|
|
72
|
+
schema.analyticsEventDailyRollups,
|
|
73
|
+
scope,
|
|
74
|
+
includeLegacyOwnerRows,
|
|
75
|
+
),
|
|
76
|
+
window,
|
|
77
|
+
),
|
|
78
|
+
windowPredicate(
|
|
79
|
+
schema.analyticsUserDays,
|
|
80
|
+
scopePredicate(schema.analyticsUserDays, scope, includeLegacyOwnerRows),
|
|
81
|
+
window,
|
|
82
|
+
),
|
|
83
|
+
];
|
|
84
|
+
const [eventRows, dailyRollupRows, userDayRows] = await Promise.all([
|
|
85
|
+
countScopedRows(schema.analyticsEvents, predicates[0]),
|
|
86
|
+
countScopedRows(schema.analyticsEventDailyRollups, predicates[1]),
|
|
87
|
+
countScopedRows(schema.analyticsUserDays, predicates[2]),
|
|
88
|
+
]);
|
|
89
|
+
return { eventRows, dailyRollupRows, userDayRows };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function purgeFirstPartyAnalyticsPostgresRows(
|
|
93
|
+
scope: FirstPartyAnalyticsPurgeScope,
|
|
94
|
+
includeLegacyOwnerRows: boolean,
|
|
95
|
+
window: FirstPartyAnalyticsPurgeWindow,
|
|
96
|
+
): Promise<FirstPartyAnalyticsPostgresPurgeCounts> {
|
|
97
|
+
const counts = await countFirstPartyAnalyticsPostgresRows(
|
|
98
|
+
scope,
|
|
99
|
+
includeLegacyOwnerRows,
|
|
100
|
+
window,
|
|
101
|
+
);
|
|
102
|
+
await (getDb() as any).transaction(async (tx: any) => {
|
|
103
|
+
await tx
|
|
104
|
+
.delete(schema.analyticsEvents)
|
|
105
|
+
.where(
|
|
106
|
+
windowPredicate(
|
|
107
|
+
schema.analyticsEvents,
|
|
108
|
+
scopePredicate(schema.analyticsEvents, scope, includeLegacyOwnerRows),
|
|
109
|
+
window,
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
await tx
|
|
113
|
+
.delete(schema.analyticsEventDailyRollups)
|
|
114
|
+
.where(
|
|
115
|
+
windowPredicate(
|
|
116
|
+
schema.analyticsEventDailyRollups,
|
|
117
|
+
scopePredicate(
|
|
118
|
+
schema.analyticsEventDailyRollups,
|
|
119
|
+
scope,
|
|
120
|
+
includeLegacyOwnerRows,
|
|
121
|
+
),
|
|
122
|
+
window,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
await tx
|
|
126
|
+
.delete(schema.analyticsUserDays)
|
|
127
|
+
.where(
|
|
128
|
+
windowPredicate(
|
|
129
|
+
schema.analyticsUserDays,
|
|
130
|
+
scopePredicate(
|
|
131
|
+
schema.analyticsUserDays,
|
|
132
|
+
scope,
|
|
133
|
+
includeLegacyOwnerRows,
|
|
134
|
+
),
|
|
135
|
+
window,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
return counts;
|
|
140
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { schema } from "../db/index.js";
|
|
4
|
+
|
|
5
|
+
export const FIRST_PARTY_POSTGRES_EVENT_VOLUME_LIMIT_ENV =
|
|
6
|
+
"ANALYTICS_FIRST_PARTY_POSTGRES_EVENT_VOLUME_LIMIT";
|
|
7
|
+
export const FIRST_PARTY_POSTGRES_EVENT_VOLUME_WINDOW_DAYS_ENV =
|
|
8
|
+
"ANALYTICS_FIRST_PARTY_POSTGRES_EVENT_VOLUME_WINDOW_DAYS";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_FIRST_PARTY_POSTGRES_EVENT_VOLUME_LIMIT = 1_000_000;
|
|
11
|
+
export const DEFAULT_FIRST_PARTY_POSTGRES_EVENT_VOLUME_WINDOW_DAYS = 30;
|
|
12
|
+
|
|
13
|
+
const MAX_EVENT_VOLUME_LIMIT = 100_000_000;
|
|
14
|
+
const MIN_VOLUME_WINDOW_DAYS = 1;
|
|
15
|
+
const MAX_VOLUME_WINDOW_DAYS = 90;
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
export interface FirstPartyPostgresEventVolumeConfig {
|
|
19
|
+
eventLimit: number;
|
|
20
|
+
windowDays: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FirstPartyPostgresEventVolumeScope {
|
|
24
|
+
ownerEmail: string;
|
|
25
|
+
orgId: string | null;
|
|
26
|
+
receivedAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FirstPartyPostgresEventVolumeReservation {
|
|
30
|
+
tenantKey: string;
|
|
31
|
+
windowStart: string;
|
|
32
|
+
eventCount: number;
|
|
33
|
+
eventLimit: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class FirstPartyPostgresEventVolumeLimitError extends Error {
|
|
37
|
+
readonly statusCode = 429;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
readonly reservation: FirstPartyPostgresEventVolumeReservation,
|
|
41
|
+
requested: number,
|
|
42
|
+
) {
|
|
43
|
+
super(
|
|
44
|
+
`Postgres first-party Analytics volume limit reached for this ${reservation.windowStart} window: ${reservation.eventCount}/${reservation.eventLimit} events reserved; connect your own analytics database or BigQuery before sending more events (requested ${requested}).`,
|
|
45
|
+
);
|
|
46
|
+
this.name = "FirstPartyPostgresEventVolumeLimitError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseIntegerEnv(
|
|
51
|
+
environment: Record<string, string | undefined>,
|
|
52
|
+
key: string,
|
|
53
|
+
fallback: number,
|
|
54
|
+
minimum: number,
|
|
55
|
+
maximum: number,
|
|
56
|
+
): number {
|
|
57
|
+
const raw = environment[key]?.trim();
|
|
58
|
+
if (!raw) return fallback;
|
|
59
|
+
if (!/^\d+$/.test(raw)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${key} must be an integer between ${minimum} and ${maximum}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const value = Number(raw);
|
|
65
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${key} must be an integer between ${minimum} and ${maximum}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getFirstPartyPostgresEventVolumeConfig(
|
|
74
|
+
environment: Record<string, string | undefined> = process.env,
|
|
75
|
+
): FirstPartyPostgresEventVolumeConfig {
|
|
76
|
+
return {
|
|
77
|
+
eventLimit: parseIntegerEnv(
|
|
78
|
+
environment,
|
|
79
|
+
FIRST_PARTY_POSTGRES_EVENT_VOLUME_LIMIT_ENV,
|
|
80
|
+
DEFAULT_FIRST_PARTY_POSTGRES_EVENT_VOLUME_LIMIT,
|
|
81
|
+
1,
|
|
82
|
+
MAX_EVENT_VOLUME_LIMIT,
|
|
83
|
+
),
|
|
84
|
+
windowDays: parseIntegerEnv(
|
|
85
|
+
environment,
|
|
86
|
+
FIRST_PARTY_POSTGRES_EVENT_VOLUME_WINDOW_DAYS_ENV,
|
|
87
|
+
DEFAULT_FIRST_PARTY_POSTGRES_EVENT_VOLUME_WINDOW_DAYS,
|
|
88
|
+
MIN_VOLUME_WINDOW_DAYS,
|
|
89
|
+
MAX_VOLUME_WINDOW_DAYS,
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function firstPartyPostgresEventVolumeWindowStart(
|
|
95
|
+
receivedAt: string,
|
|
96
|
+
windowDays: number,
|
|
97
|
+
): string {
|
|
98
|
+
if (!Number.isInteger(windowDays) || windowDays < 1) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"First-party Analytics volume window must be a positive integer",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const receivedAtMs = Date.parse(receivedAt);
|
|
104
|
+
if (!Number.isFinite(receivedAtMs)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"First-party Analytics volume receivedAt must be an ISO timestamp",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const windowMs = windowDays * DAY_MS;
|
|
110
|
+
return new Date(Math.floor(receivedAtMs / windowMs) * windowMs).toISOString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function tenantKey(
|
|
114
|
+
scope: Pick<FirstPartyPostgresEventVolumeScope, "ownerEmail" | "orgId">,
|
|
115
|
+
): string {
|
|
116
|
+
return scope.orgId ? `org:${scope.orgId}` : `user:${scope.ownerEmail}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function reservationId(tenant: string, windowStart: string): string {
|
|
120
|
+
return `aevu_${encodeURIComponent(tenant)}_${encodeURIComponent(windowStart)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function reserveFirstPartyPostgresEventVolume(
|
|
124
|
+
transaction: any,
|
|
125
|
+
scope: FirstPartyPostgresEventVolumeScope,
|
|
126
|
+
requested: number,
|
|
127
|
+
): Promise<FirstPartyPostgresEventVolumeReservation> {
|
|
128
|
+
if (!Number.isInteger(requested) || requested < 1) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"First-party Analytics volume reservation must be positive",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const config = getFirstPartyPostgresEventVolumeConfig();
|
|
134
|
+
const scopedTenantKey = tenantKey(scope);
|
|
135
|
+
const windowStart = firstPartyPostgresEventVolumeWindowStart(
|
|
136
|
+
scope.receivedAt,
|
|
137
|
+
config.windowDays,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await transaction
|
|
141
|
+
.insert(schema.analyticsEventVolumeUsage)
|
|
142
|
+
.values({
|
|
143
|
+
id: reservationId(scopedTenantKey, windowStart),
|
|
144
|
+
tenantKey: scopedTenantKey,
|
|
145
|
+
ownerEmail: scope.ownerEmail,
|
|
146
|
+
orgId: scope.orgId,
|
|
147
|
+
windowStart,
|
|
148
|
+
eventCount: 0,
|
|
149
|
+
eventLimit: config.eventLimit,
|
|
150
|
+
updatedAt: scope.receivedAt,
|
|
151
|
+
})
|
|
152
|
+
.onConflictDoNothing({
|
|
153
|
+
target: [
|
|
154
|
+
schema.analyticsEventVolumeUsage.tenantKey,
|
|
155
|
+
schema.analyticsEventVolumeUsage.windowStart,
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const [updated] = await transaction
|
|
160
|
+
.update(schema.analyticsEventVolumeUsage)
|
|
161
|
+
.set({
|
|
162
|
+
eventCount: sql`${schema.analyticsEventVolumeUsage.eventCount} + ${requested}`,
|
|
163
|
+
eventLimit: config.eventLimit,
|
|
164
|
+
updatedAt: scope.receivedAt,
|
|
165
|
+
})
|
|
166
|
+
.where(
|
|
167
|
+
and(
|
|
168
|
+
eq(schema.analyticsEventVolumeUsage.tenantKey, scopedTenantKey),
|
|
169
|
+
eq(schema.analyticsEventVolumeUsage.windowStart, windowStart),
|
|
170
|
+
sql`${schema.analyticsEventVolumeUsage.eventCount} + ${requested} <= ${config.eventLimit}`,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
.returning({
|
|
174
|
+
eventCount: schema.analyticsEventVolumeUsage.eventCount,
|
|
175
|
+
eventLimit: schema.analyticsEventVolumeUsage.eventLimit,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!updated) {
|
|
179
|
+
const [current] = await transaction
|
|
180
|
+
.select({
|
|
181
|
+
eventCount: schema.analyticsEventVolumeUsage.eventCount,
|
|
182
|
+
eventLimit: schema.analyticsEventVolumeUsage.eventLimit,
|
|
183
|
+
})
|
|
184
|
+
.from(schema.analyticsEventVolumeUsage)
|
|
185
|
+
.where(
|
|
186
|
+
and(
|
|
187
|
+
eq(schema.analyticsEventVolumeUsage.tenantKey, scopedTenantKey),
|
|
188
|
+
eq(schema.analyticsEventVolumeUsage.windowStart, windowStart),
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
.limit(1);
|
|
192
|
+
const reservation = {
|
|
193
|
+
tenantKey: scopedTenantKey,
|
|
194
|
+
windowStart,
|
|
195
|
+
eventCount: Number(current?.eventCount ?? 0),
|
|
196
|
+
eventLimit: Number(current?.eventLimit ?? config.eventLimit),
|
|
197
|
+
};
|
|
198
|
+
throw new FirstPartyPostgresEventVolumeLimitError(reservation, requested);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
tenantKey: scopedTenantKey,
|
|
203
|
+
windowStart,
|
|
204
|
+
eventCount: Number(updated.eventCount),
|
|
205
|
+
eventLimit: Number(updated.eventLimit),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
recordFirstPartyAnalyticsQueryPressure,
|
|
26
26
|
} from "./first-party-analytics-health.js";
|
|
27
27
|
import { upsertFirstPartyAnalyticsRollups } from "./first-party-analytics-rollups.js";
|
|
28
|
+
import { reserveFirstPartyPostgresEventVolume } from "./first-party-analytics-volume.js";
|
|
28
29
|
|
|
29
30
|
export interface AnalyticsScope {
|
|
30
31
|
userEmail: string;
|
|
@@ -578,6 +579,7 @@ export async function recordAnalyticsEvents(
|
|
|
578
579
|
orgId: key.orgId ?? null,
|
|
579
580
|
});
|
|
580
581
|
|
|
582
|
+
let bigQueryInsertError: unknown = null;
|
|
581
583
|
if (rows.length && (backend.sink === "dual" || backend.sink === "bigquery")) {
|
|
582
584
|
try {
|
|
583
585
|
await runWithRequestContext(
|
|
@@ -588,21 +590,47 @@ export async function recordAnalyticsEvents(
|
|
|
588
590
|
() => insertFirstPartyAnalyticsRows(rows, backend.table),
|
|
589
591
|
);
|
|
590
592
|
} catch (error) {
|
|
591
|
-
if (backend.sink === "bigquery")
|
|
593
|
+
if (backend.sink === "bigquery") {
|
|
594
|
+
// Keep SQL-only exception issues, public-key metadata, and session
|
|
595
|
+
// replay links durable even when the warehouse is temporarily down.
|
|
596
|
+
// The request still fails below so callers do not mistake a warehouse
|
|
597
|
+
// outage for a successful BigQuery write.
|
|
598
|
+
bigQueryInsertError = error;
|
|
599
|
+
}
|
|
592
600
|
// Dual-write mode keeps Postgres as the recoverable source until the
|
|
593
601
|
// backfill has completed. A BigQuery outage must not lose live events.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
602
|
+
if (backend.sink === "dual") {
|
|
603
|
+
console.error(
|
|
604
|
+
"[first-party-analytics] BigQuery dual-write failed; retaining Postgres event:",
|
|
605
|
+
error,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
598
608
|
}
|
|
599
609
|
}
|
|
600
610
|
|
|
611
|
+
let postgresInsertError: unknown = null;
|
|
601
612
|
if (rows.length && backend.sink !== "bigquery") {
|
|
602
|
-
|
|
603
|
-
await
|
|
604
|
-
|
|
605
|
-
|
|
613
|
+
try {
|
|
614
|
+
await db.transaction(async (tx: any) => {
|
|
615
|
+
if (backend.sink === "postgres" || backend.sink === "dual") {
|
|
616
|
+
await reserveFirstPartyPostgresEventVolume(
|
|
617
|
+
tx,
|
|
618
|
+
{
|
|
619
|
+
ownerEmail: key.ownerEmail,
|
|
620
|
+
orgId: key.orgId ?? null,
|
|
621
|
+
receivedAt,
|
|
622
|
+
},
|
|
623
|
+
rows.length,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
await tx.insert(schema.analyticsEvents).values(rows);
|
|
627
|
+
await upsertFirstPartyAnalyticsRollups(rows, tx);
|
|
628
|
+
});
|
|
629
|
+
} catch (error) {
|
|
630
|
+
// Preserve SQL-only exception issues and public-key metadata below even
|
|
631
|
+
// when a Postgres volume reservation or insert rejects the batch.
|
|
632
|
+
postgresInsertError = error;
|
|
633
|
+
}
|
|
606
634
|
}
|
|
607
635
|
if (rows.length) {
|
|
608
636
|
await touchPublicKeyLastUsedAt(key.id, receivedAt);
|
|
@@ -627,6 +655,9 @@ export async function recordAnalyticsEvents(
|
|
|
627
655
|
}
|
|
628
656
|
}
|
|
629
657
|
|
|
658
|
+
if (bigQueryInsertError) throw bigQueryInsertError;
|
|
659
|
+
if (postgresInsertError) throw postgresInsertError;
|
|
660
|
+
|
|
630
661
|
return { accepted: rows.length, keyId: key.id };
|
|
631
662
|
}
|
|
632
663
|
|