@agent-native/core 0.161.1 → 0.161.4
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/templates/analytics/actions/compose-dashboard.ts +5 -2
- package/corpus/templates/analytics/actions/export-dashboard-panel-to-google-sheet.ts +11 -5
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +89 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +14 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +89 -41
- package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +8 -3
- package/corpus/templates/analytics/server/lib/error-capture.ts +6 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +187 -44
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -16
- package/corpus/templates/clips/actions/save-browser-transcript.ts +20 -4
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +167 -35
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +8 -1
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +39 -2
- package/corpus/templates/slides/actions/list-decks.ts +36 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +17 -6
- package/dist/agent/engine/builder-engine.js +37 -19
- package/dist/agent/engine/types.d.ts +29 -0
- package/dist/agent/engine/types.js +6 -0
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/run-manager.d.ts +6 -0
- package/dist/agent/run-manager.js +27 -0
- package/dist/agent/run-store.d.ts +5 -5
- package/dist/agent/run-store.js +52 -15
- package/dist/client/AssistantChat.d.ts +1 -0
- package/dist/client/AssistantChat.js +10 -1
- package/dist/db/client.js +10 -2
- package/dist/db/create-get-db.js +42 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/onboarding-html.js +22 -77
- package/dist/server/poll.d.ts +5 -5
- package/dist/server/poll.js +19 -26
- package/dist/server/release-migrations.js +4 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/auth-copy.d.ts +7 -0
- package/dist/shared/auth-copy.js +77 -0
- package/dist/shared/mcp-embed-headers.js +8 -4
- package/dist/vite/client.js +94 -3
- package/dist/workspace-connections/migrations.d.ts +18 -0
- package/dist/workspace-connections/migrations.js +153 -0
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
upsertDashboard,
|
|
14
14
|
upsertDashboardWithRetry,
|
|
15
15
|
} from "../server/lib/dashboards-store";
|
|
16
|
-
import {
|
|
16
|
+
import { validateFirstPartyAnalyticsSqlForScope } from "../server/lib/first-party-analytics.js";
|
|
17
17
|
import {
|
|
18
18
|
buildFirstPartyDashboardFilters,
|
|
19
19
|
buildPanel,
|
|
@@ -212,7 +212,10 @@ export default defineAction({
|
|
|
212
212
|
invalidMetrics.push({ metric: req.metric, reason: timeScopeError });
|
|
213
213
|
continue;
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
await validateFirstPartyAnalyticsSqlForScope(panel.sql, {
|
|
216
|
+
userEmail: ctx.email,
|
|
217
|
+
orgId: ctx.orgId,
|
|
218
|
+
});
|
|
216
219
|
} catch (e: any) {
|
|
217
220
|
invalidMetrics.push({
|
|
218
221
|
metric: req.metric,
|
|
@@ -31,14 +31,20 @@ function asPanels(config: Record<string, unknown>): DashboardPanel[] {
|
|
|
31
31
|
: [];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Any structured panel failure (`missing_api_key`, `unsupported_by_backend`)
|
|
36
|
+
* carries its own explanation. Surface that rather than the generic
|
|
37
|
+
* invalid-result message below, which reads as a bug in the export.
|
|
38
|
+
*/
|
|
39
|
+
function panelFailureMessage(result: unknown): string | null {
|
|
35
40
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
36
41
|
return null;
|
|
37
42
|
}
|
|
38
43
|
const value = result as { error?: unknown; message?: unknown };
|
|
39
|
-
|
|
44
|
+
if (typeof value.error !== "string" || !value.error) return null;
|
|
45
|
+
return typeof value.message === "string" && value.message
|
|
40
46
|
? value.message
|
|
41
|
-
:
|
|
47
|
+
: value.error;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export default defineAction({
|
|
@@ -97,8 +103,8 @@ export default defineAction({
|
|
|
97
103
|
{ source: panel.source, query },
|
|
98
104
|
context,
|
|
99
105
|
);
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
106
|
+
const failure = panelFailureMessage(result);
|
|
107
|
+
if (failure) throw new Error(failure);
|
|
102
108
|
if (
|
|
103
109
|
!result ||
|
|
104
110
|
typeof result !== "object" ||
|
|
@@ -9,13 +9,79 @@ import {
|
|
|
9
9
|
getFirstPartyAnalyticsBigQueryBackfillJob,
|
|
10
10
|
queueFirstPartyAnalyticsBigQueryBackfill,
|
|
11
11
|
} from "../server/jobs/analytics-bigquery-backfill.js";
|
|
12
|
+
import { listDashboards } from "../server/lib/dashboards-store.js";
|
|
12
13
|
import { requireAnalyticsAdminContext } from "../server/lib/db-admin-connections.js";
|
|
13
14
|
import {
|
|
14
15
|
assertFirstPartyAnalyticsBigQueryReady,
|
|
16
|
+
assertFirstPartyAnalyticsBigQuerySql,
|
|
17
|
+
FirstPartyAnalyticsUnsupportedSqlError,
|
|
18
|
+
type FirstPartyAnalyticsScope,
|
|
15
19
|
getFirstPartyAnalyticsBackend,
|
|
16
20
|
saveFirstPartyAnalyticsBackend,
|
|
17
21
|
} from "../server/lib/first-party-analytics-backend.js";
|
|
18
22
|
|
|
23
|
+
interface UnrunnablePanel {
|
|
24
|
+
dashboardId: string;
|
|
25
|
+
dashboardName: string;
|
|
26
|
+
panelId: string;
|
|
27
|
+
panelTitle: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stored first-party panels the BigQuery sink cannot run. Cutover only flips a
|
|
33
|
+
* setting — it never rewrites saved SQL — so every panel listed here starts
|
|
34
|
+
* failing the moment the flip lands. Surfacing the list is the whole point: the
|
|
35
|
+
* alternative is the org discovering it as broken panels afterwards.
|
|
36
|
+
*/
|
|
37
|
+
async function findPanelsUnrunnableOnBigQuery(
|
|
38
|
+
scope: FirstPartyAnalyticsScope,
|
|
39
|
+
): Promise<UnrunnablePanel[]> {
|
|
40
|
+
const dashboards = await listDashboards(
|
|
41
|
+
{ email: scope.userEmail, orgId: scope.orgId },
|
|
42
|
+
{ kind: "sql", archived: "all" },
|
|
43
|
+
);
|
|
44
|
+
const unrunnable: UnrunnablePanel[] = [];
|
|
45
|
+
for (const dashboard of dashboards) {
|
|
46
|
+
const panels = Array.isArray(dashboard.config?.panels)
|
|
47
|
+
? dashboard.config.panels
|
|
48
|
+
: [];
|
|
49
|
+
for (const raw of panels) {
|
|
50
|
+
const panel = raw as Record<string, unknown>;
|
|
51
|
+
if (panel?.source !== "first-party") continue;
|
|
52
|
+
if (typeof panel.sql !== "string" || !panel.sql.trim()) continue;
|
|
53
|
+
try {
|
|
54
|
+
// Stored `{{var}}` tokens are left uninterpolated on purpose: the
|
|
55
|
+
// translator rejects syntax, not values, and filter values are never
|
|
56
|
+
// syntax. Interpolating here would drag the dashboard-save module — and
|
|
57
|
+
// its whole import chain — into this action.
|
|
58
|
+
assertFirstPartyAnalyticsBigQuerySql(panel.sql);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
unrunnable.push({
|
|
61
|
+
dashboardId: dashboard.id,
|
|
62
|
+
dashboardName: dashboard.title,
|
|
63
|
+
panelId: typeof panel.id === "string" ? panel.id : "(unnamed)",
|
|
64
|
+
panelTitle: typeof panel.title === "string" ? panel.title : "",
|
|
65
|
+
reason:
|
|
66
|
+
error instanceof FirstPartyAnalyticsUnsupportedSqlError
|
|
67
|
+
? `uses ${error.construct}`
|
|
68
|
+
: `${(error as Error)?.message ?? String(error)}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return unrunnable;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function describeUnrunnablePanels(panels: UnrunnablePanel[]): string {
|
|
77
|
+
return panels
|
|
78
|
+
.map(
|
|
79
|
+
(panel) =>
|
|
80
|
+
`${panel.dashboardId}/${panel.panelId} "${panel.panelTitle}" ${panel.reason}`,
|
|
81
|
+
)
|
|
82
|
+
.join("; ");
|
|
83
|
+
}
|
|
84
|
+
|
|
19
85
|
async function resolveScope() {
|
|
20
86
|
const userEmail = getRequestUserEmail();
|
|
21
87
|
if (!userEmail) throw new Error("no authenticated user");
|
|
@@ -48,15 +114,21 @@ const migrationSchema = z.object({
|
|
|
48
114
|
"Optional per-page limit used when mode is prepare; for an existing job, it can only increase the bounded batch size. The durable worker then processes non-overlapping UTC time shards in parallel.",
|
|
49
115
|
),
|
|
50
116
|
confirm: z.boolean().optional(),
|
|
117
|
+
acknowledgeUnrunnablePanels: z
|
|
118
|
+
.boolean()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe(
|
|
121
|
+
"Cut over even though status reported saved first-party panels the BigQuery backend cannot run. Those panels render an unsupported-backend state until their SQL is rewritten; nothing migrates or rewrites them.",
|
|
122
|
+
),
|
|
51
123
|
});
|
|
52
124
|
|
|
53
125
|
export default defineAction({
|
|
54
126
|
description:
|
|
55
|
-
"Production migration for the current Analytics organization. Run prepare to validate the configured BigQuery table, enter dual-write mode, and enqueue a durable worker. The worker creates non-overlapping UTC time shards, processes the newest shards first with bounded parallel leases, persists per-shard cursors, and pauses on database pressure. Call status or backfill to inspect progress, then call cutover with confirm=true only after the worker reports completed. New /track events stay in Postgres during dual-write, so a BigQuery outage does not silently lose live data. Cutover is the only step that stops first-party event and rollup writes to Postgres; public-key metadata, derived exception issues, and session-replay data remain in the SQL store.",
|
|
127
|
+
"Production migration for the current Analytics organization. Run prepare to validate the configured BigQuery table, enter dual-write mode, and enqueue a durable worker. The worker creates non-overlapping UTC time shards, processes the newest shards first with bounded parallel leases, persists per-shard cursors, and pauses on database pressure. Call status or backfill to inspect progress, then call cutover with confirm=true only after the worker reports completed. status and cutover both list saved first-party dashboard panels whose SQL the BigQuery backend cannot run; cutover refuses to flip while any exist unless acknowledgeUnrunnablePanels=true, because the flip never rewrites stored SQL. New /track events stay in Postgres during dual-write, so a BigQuery outage does not silently lose live data. Cutover is the only step that stops first-party event and rollup writes to Postgres; public-key metadata, derived exception issues, and session-replay data remain in the SQL store.",
|
|
56
128
|
schema: migrationSchema,
|
|
57
129
|
agentTool: false,
|
|
58
130
|
needsApproval: ({ mode }) => mode === "cutover",
|
|
59
|
-
run: async ({ mode, table, limit, confirm }) => {
|
|
131
|
+
run: async ({ mode, table, limit, confirm, acknowledgeUnrunnablePanels }) => {
|
|
60
132
|
const scope = await resolveScope();
|
|
61
133
|
const current = await getFirstPartyAnalyticsBackend(scope);
|
|
62
134
|
const configuredTable = table ?? current.table;
|
|
@@ -70,8 +142,13 @@ export default defineAction({
|
|
|
70
142
|
current.sink === "postgres"
|
|
71
143
|
? null
|
|
72
144
|
: await getFirstPartyAnalyticsBigQueryBackfillJob(scope);
|
|
145
|
+
const unrunnablePanels =
|
|
146
|
+
current.sink === "bigquery"
|
|
147
|
+
? []
|
|
148
|
+
: await findPanelsUnrunnableOnBigQuery(scope);
|
|
73
149
|
return {
|
|
74
150
|
...current,
|
|
151
|
+
unrunnablePanels,
|
|
75
152
|
backfillCursor:
|
|
76
153
|
current.sink === "dual"
|
|
77
154
|
? (job?.cursor ?? null)
|
|
@@ -171,6 +248,15 @@ export default defineAction({
|
|
|
171
248
|
);
|
|
172
249
|
}
|
|
173
250
|
const ready = await assertFirstPartyAnalyticsBigQueryReady(current.table);
|
|
251
|
+
const unrunnablePanels = await findPanelsUnrunnableOnBigQuery(scope);
|
|
252
|
+
if (unrunnablePanels.length && !acknowledgeUnrunnablePanels) {
|
|
253
|
+
throw Object.assign(
|
|
254
|
+
new Error(
|
|
255
|
+
`${unrunnablePanels.length} saved first-party panel(s) cannot run on BigQuery and would start failing at cutover: ${describeUnrunnablePanels(unrunnablePanels)}. Rewrite them in BigQuery-compatible SQL, or pass acknowledgeUnrunnablePanels=true to cut over and leave them showing an unsupported-backend state.`,
|
|
256
|
+
),
|
|
257
|
+
{ unrunnablePanels },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
174
260
|
await saveFirstPartyAnalyticsBackend(scope, {
|
|
175
261
|
sink: "bigquery",
|
|
176
262
|
table: ready.table.fullyQualified,
|
|
@@ -182,6 +268,7 @@ export default defineAction({
|
|
|
182
268
|
table: ready.table.fullyQualified,
|
|
183
269
|
existingBigQueryRows: ready.rowCount,
|
|
184
270
|
postgresEventWrites: "stopped",
|
|
271
|
+
unrunnablePanels,
|
|
185
272
|
next: "verify dashboards",
|
|
186
273
|
};
|
|
187
274
|
},
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
upsertDashboardWithRetry,
|
|
16
16
|
} from "../server/lib/dashboards-store";
|
|
17
17
|
import { parseDemoDescriptor } from "../server/lib/demo-source";
|
|
18
|
-
import {
|
|
18
|
+
import { FirstPartyAnalyticsUnsupportedSqlError } from "../server/lib/first-party-analytics-backend.js";
|
|
19
|
+
import { validateFirstPartyAnalyticsSqlForScope } from "../server/lib/first-party-analytics.js";
|
|
19
20
|
import { DASHBOARD_SQL_VALIDATION_TIMEOUT_MS } from "../shared/dashboard-report-timeouts.js";
|
|
20
21
|
import {
|
|
21
22
|
applyPanelOrder,
|
|
@@ -391,6 +392,11 @@ export function validateDashboardConfig(
|
|
|
391
392
|
|
|
392
393
|
const MAX_CONCURRENT_SQL_VALIDATIONS = 8;
|
|
393
394
|
|
|
395
|
+
function firstPartyScope() {
|
|
396
|
+
const { email, orgId } = resolveScope();
|
|
397
|
+
return { userEmail: email, orgId };
|
|
398
|
+
}
|
|
399
|
+
|
|
394
400
|
export interface ValidatePanelSqlOptions {
|
|
395
401
|
signal?: AbortSignal;
|
|
396
402
|
}
|
|
@@ -445,7 +451,10 @@ export async function validatePanelSql(
|
|
|
445
451
|
i,
|
|
446
452
|
);
|
|
447
453
|
if (timeScopeError) return timeScopeError;
|
|
448
|
-
|
|
454
|
+
await validateFirstPartyAnalyticsSqlForScope(
|
|
455
|
+
interpolate(raw, vars),
|
|
456
|
+
firstPartyScope(),
|
|
457
|
+
);
|
|
449
458
|
} catch (e: any) {
|
|
450
459
|
if (
|
|
451
460
|
typeof e?.message === "string" &&
|
|
@@ -453,6 +462,9 @@ export async function validatePanelSql(
|
|
|
453
462
|
) {
|
|
454
463
|
return e.message;
|
|
455
464
|
}
|
|
465
|
+
if (e instanceof FirstPartyAnalyticsUnsupportedSqlError) {
|
|
466
|
+
return `panel[${i}] "${p.title || p.id}" cannot run on this scope's active data backend (BigQuery) because its SQL uses ${e.construct}. Rewrite it with BigQuery-compatible SQL, or move the scope back to the PostgreSQL backend.`;
|
|
467
|
+
}
|
|
456
468
|
return `panel[${i}] "${p.title || p.id}" first-party analytics SQL is invalid: ${e?.message ?? e}`;
|
|
457
469
|
}
|
|
458
470
|
}
|
|
@@ -104,6 +104,12 @@ function parseProgramDescriptor(sql: string): {
|
|
|
104
104
|
paramsText: string;
|
|
105
105
|
} {
|
|
106
106
|
if (!sql.trim()) return { programId: "", paramsText: "" };
|
|
107
|
+
// A panel may be stored as the bare program id — the server accepts that as a
|
|
108
|
+
// complete descriptor, so the editor has to show it rather than blanking the
|
|
109
|
+
// picker on a JSON.parse it was never going to satisfy.
|
|
110
|
+
if (/^dp_[A-Za-z0-9]+$/.test(sql.trim())) {
|
|
111
|
+
return { programId: sql.trim(), paramsText: "" };
|
|
112
|
+
}
|
|
107
113
|
try {
|
|
108
114
|
const parsed = JSON.parse(sql) as { programId?: unknown; params?: unknown };
|
|
109
115
|
const programId =
|
|
@@ -10,6 +10,7 @@ import { getUserSegmentation, queryEvents } from "./amplitude";
|
|
|
10
10
|
import { runQuery } from "./bigquery";
|
|
11
11
|
import { runDemoPanel, serializeDemoDescriptorInput } from "./demo-source";
|
|
12
12
|
import { queryFirstPartyAnalytics } from "./first-party-analytics";
|
|
13
|
+
import { FirstPartyAnalyticsUnsupportedSqlError } from "./first-party-analytics-backend";
|
|
13
14
|
import { runReport } from "./google-analytics";
|
|
14
15
|
import {
|
|
15
16
|
runPrometheusPanel,
|
|
@@ -36,6 +37,19 @@ export interface DashboardPanelQueryResult {
|
|
|
36
37
|
bytesProcessed?: number;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A stored panel the active data backend cannot run at all. It carries no
|
|
42
|
+
* `rows` on purpose: an empty result set would be indistinguishable from a
|
|
43
|
+
* query that legitimately matched nothing, and this panel matched nothing
|
|
44
|
+
* because it never ran.
|
|
45
|
+
*/
|
|
46
|
+
export interface UnsupportedBackendResponse {
|
|
47
|
+
error: "unsupported_by_backend";
|
|
48
|
+
backend: "bigquery";
|
|
49
|
+
construct: string;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export function isDashboardPanelSource(
|
|
40
54
|
value: unknown,
|
|
41
55
|
): value is DashboardPanelSource {
|
|
@@ -45,43 +59,48 @@ export function isDashboardPanelSource(
|
|
|
45
59
|
);
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
export interface ProgramDescriptor {
|
|
63
|
+
programId: string;
|
|
64
|
+
params?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Stored data program id: `dp_` + a random hex id. */
|
|
68
|
+
const PROGRAM_ID_PATTERN = /^dp_[A-Za-z0-9]+$/;
|
|
69
|
+
|
|
48
70
|
/**
|
|
49
71
|
* program panels carry a JSON blob in `sql` describing which stored data
|
|
50
72
|
* program to run and with what params. Shape:
|
|
51
73
|
* { programId: string; params?: Record<string, unknown> }.
|
|
74
|
+
*
|
|
75
|
+
* A bare program id is part of that grammar, not a fallback: with no params the
|
|
76
|
+
* id IS the whole descriptor, and it is what every caller reaches for first.
|
|
77
|
+
* Writer and reader both resolve through here so they cannot disagree — the
|
|
78
|
+
* writer used to pass any string straight through while the reader required
|
|
79
|
+
* JSON, so a panel saved as `dp_01c5e3d...` threw `is not valid JSON` on every
|
|
80
|
+
* render instead of rendering. Anything that is neither a descriptor nor an id
|
|
81
|
+
* still throws; an unreadable panel must never resolve to an empty one.
|
|
52
82
|
*/
|
|
53
|
-
export function
|
|
54
|
-
if (typeof raw === "string")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (typeof obj.programId !== "string" || !obj.programId.trim()) {
|
|
83
|
+
export function coerceProgramDescriptor(raw: unknown): ProgramDescriptor {
|
|
84
|
+
if (typeof raw === "string") {
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
if (!trimmed) {
|
|
58
87
|
throw new Error("program panel descriptor requires a 'programId' field");
|
|
59
88
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function parseProgramDescriptor(raw: string): ProgramDescriptor {
|
|
73
|
-
let parsed: unknown;
|
|
74
|
-
try {
|
|
75
|
-
parsed = JSON.parse(raw);
|
|
76
|
-
} catch (err: any) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`program panel sql must be a JSON object: ${err?.message ?? err}`,
|
|
79
|
-
);
|
|
89
|
+
if (PROGRAM_ID_PATTERN.test(trimmed)) return { programId: trimmed };
|
|
90
|
+
let parsed: unknown;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(trimmed);
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`program panel sql must be a program id or a JSON object: ${err?.message ?? err}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return coerceProgramDescriptor(parsed);
|
|
80
99
|
}
|
|
81
|
-
if (!
|
|
82
|
-
throw new Error("program panel sql must be a JSON object");
|
|
100
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
101
|
+
throw new Error("program panel sql must be a program id or a JSON object");
|
|
83
102
|
}
|
|
84
|
-
const obj =
|
|
103
|
+
const obj = raw as Record<string, unknown>;
|
|
85
104
|
if (typeof obj.programId !== "string" || !obj.programId.trim()) {
|
|
86
105
|
throw new Error("program panel descriptor requires a 'programId' field");
|
|
87
106
|
}
|
|
@@ -89,7 +108,15 @@ function parseProgramDescriptor(raw: string): ProgramDescriptor {
|
|
|
89
108
|
obj.params && typeof obj.params === "object" && !Array.isArray(obj.params)
|
|
90
109
|
? (obj.params as Record<string, unknown>)
|
|
91
110
|
: undefined;
|
|
92
|
-
return { programId: obj.programId, params };
|
|
111
|
+
return { programId: obj.programId.trim(), ...(params ? { params } : {}) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function serializeProgramDescriptorInput(raw: unknown): string {
|
|
115
|
+
return JSON.stringify(coerceProgramDescriptor(raw));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseProgramDescriptor(raw: string): ProgramDescriptor {
|
|
119
|
+
return coerceProgramDescriptor(raw);
|
|
93
120
|
}
|
|
94
121
|
|
|
95
122
|
export function normalizeDashboardPanelQuery(
|
|
@@ -406,7 +433,9 @@ export async function runDashboardPanelQuery(args: {
|
|
|
406
433
|
query: string;
|
|
407
434
|
ctx: CredentialContext;
|
|
408
435
|
timeoutMs?: number;
|
|
409
|
-
}): Promise<
|
|
436
|
+
}): Promise<
|
|
437
|
+
DashboardPanelQueryResult | MissingKeyResponse | UnsupportedBackendResponse
|
|
438
|
+
> {
|
|
410
439
|
const { source, query, ctx, timeoutMs } = args;
|
|
411
440
|
|
|
412
441
|
if (source === "bigquery") {
|
|
@@ -452,17 +481,36 @@ export async function runDashboardPanelQuery(args: {
|
|
|
452
481
|
}
|
|
453
482
|
|
|
454
483
|
if (source === "first-party") {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
484
|
+
try {
|
|
485
|
+
return await queryFirstPartyAnalytics(
|
|
486
|
+
query,
|
|
487
|
+
{
|
|
488
|
+
userEmail: ctx.userEmail,
|
|
489
|
+
orgId: ctx.orgId ?? null,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
cache: true,
|
|
493
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
// Only the "no BigQuery equivalent exists" failure becomes a rendered
|
|
498
|
+
// state. Every other failure — timeout, permission, provider outage —
|
|
499
|
+
// still throws, because those are retryable and must not read to the
|
|
500
|
+
// user as a permanent property of the panel.
|
|
501
|
+
if (!(error instanceof FirstPartyAnalyticsUnsupportedSqlError))
|
|
502
|
+
throw error;
|
|
503
|
+
console.error(
|
|
504
|
+
"[first-party-analytics] Panel SQL has no BigQuery translation:",
|
|
505
|
+
error,
|
|
506
|
+
);
|
|
507
|
+
return {
|
|
508
|
+
error: "unsupported_by_backend",
|
|
509
|
+
backend: "bigquery",
|
|
510
|
+
construct: error.construct,
|
|
511
|
+
message: `This panel can't run on your current data backend (BigQuery) because its SQL uses ${error.construct}. Edit the panel's SQL, or switch the backend back to PostgreSQL.`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
466
514
|
}
|
|
467
515
|
|
|
468
516
|
if (source === "demo") {
|
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
type DashboardPanelQueryResult,
|
|
13
13
|
type DashboardPanelSource,
|
|
14
14
|
runDashboardPanelQuery,
|
|
15
|
+
type UnsupportedBackendResponse,
|
|
15
16
|
} from "./dashboard-panel-query";
|
|
16
17
|
|
|
18
|
+
type AnalyticsPanelSourceFailure =
|
|
19
|
+
| MissingKeyResponse
|
|
20
|
+
| UnsupportedBackendResponse;
|
|
21
|
+
|
|
17
22
|
type AnalyticsPanelSourceResolver = PanelSourceResolver<
|
|
18
23
|
DashboardPanelSource,
|
|
19
24
|
CredentialContext
|
|
@@ -35,7 +40,7 @@ function createResolver(
|
|
|
35
40
|
query: request.query,
|
|
36
41
|
ctx: context,
|
|
37
42
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
38
|
-
})) as DashboardPanelQueryResult |
|
|
43
|
+
})) as DashboardPanelQueryResult | AnalyticsPanelSourceFailure;
|
|
39
44
|
},
|
|
40
45
|
};
|
|
41
46
|
}
|
|
@@ -51,8 +56,8 @@ const registry = createPanelSourceResolverRegistry<
|
|
|
51
56
|
export async function resolveAnalyticsPanelSource(
|
|
52
57
|
request: AnalyticsPanelSourceRequest,
|
|
53
58
|
context: CredentialContext,
|
|
54
|
-
): Promise<PanelSourceResult |
|
|
59
|
+
): Promise<PanelSourceResult | AnalyticsPanelSourceFailure> {
|
|
55
60
|
return registry.resolve(request, context) as Promise<
|
|
56
|
-
PanelSourceResult |
|
|
61
|
+
PanelSourceResult | AnalyticsPanelSourceFailure
|
|
57
62
|
>;
|
|
58
63
|
}
|
|
@@ -396,6 +396,12 @@ function normalizeMessageForFingerprint(message: string): string {
|
|
|
396
396
|
.replace(/(['"`])[^\s'"`]*\1/g, "<str>")
|
|
397
397
|
.replace(/(?:\/[\w.@%+-]+){2,}\/?/g, "<path>")
|
|
398
398
|
.replace(/0x[0-9a-f]+/gi, "<hex>")
|
|
399
|
+
// Bare hex ids (request ids, trace ids, the gateway's "ERROR ID: ...")
|
|
400
|
+
// carry no dashes and no 0x, so they used to fall through to the digit
|
|
401
|
+
// rule below — which keeps the a-f nibbles and gives every occurrence its
|
|
402
|
+
// own key. An outage then renders as N unrelated issues of count 1.
|
|
403
|
+
// Requiring a letter AND a digit keeps prose and identifiers out.
|
|
404
|
+
.replace(/\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*\d)[0-9a-f]{8,}\b/gi, "<hex>")
|
|
399
405
|
// Not \b\d+\b: a unit suffix ("8000ms") keeps the digits word-adjacent, so
|
|
400
406
|
// a bounded rule leaves every timeout value in its own group.
|
|
401
407
|
.replace(/\d+/g, "<n>")
|