@agent-native/core 0.161.1 → 0.161.2
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/dist/agent/engine/builder-engine.js +7 -4
- package/dist/agent/engine/types.d.ts +10 -0
- package/dist/agent/engine/types.js +3 -0
- package/dist/agent/production-agent.js +1 -0
- package/dist/agent/run-manager.js +8 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- 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/release-migrations.js +4 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/workspace-connections/migrations.d.ts +18 -0
- package/dist/workspace-connections/migrations.js +153 -0
- package/package.json +3 -3
|
@@ -34,6 +34,23 @@ export interface FirstPartyAnalyticsScope {
|
|
|
34
34
|
orgId: string | null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Stored panel SQL is valid PostgreSQL but has no BigQuery equivalent. This is
|
|
39
|
+
* not a query failure: the panel cannot run for this scope until its SQL or the
|
|
40
|
+
* scope's sink changes, so retrying is pointless and callers render an
|
|
41
|
+
* explanatory state instead. `construct` names the exact syntax that has no
|
|
42
|
+
* mapping, and is the only part safe to show a user.
|
|
43
|
+
*/
|
|
44
|
+
export class FirstPartyAnalyticsUnsupportedSqlError extends Error {
|
|
45
|
+
readonly construct: string;
|
|
46
|
+
|
|
47
|
+
constructor(construct: string, message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "FirstPartyAnalyticsUnsupportedSqlError";
|
|
50
|
+
this.construct = construct;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
interface FirstPartyAnalyticsBackendSetting {
|
|
38
55
|
sink?: unknown;
|
|
39
56
|
table?: unknown;
|
|
@@ -572,6 +589,59 @@ function rewriteOutsideSqlLiterals(
|
|
|
572
589
|
return result;
|
|
573
590
|
}
|
|
574
591
|
|
|
592
|
+
const SQL_LITERAL_PLACEHOLDER_PREFIX = "_fpa_lit_";
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Same intent as `rewriteOutsideSqlLiterals`, but `rewrite` sees one string
|
|
596
|
+
* with each literal stood in for by an identifier-shaped placeholder rather
|
|
597
|
+
* than a sequence of fragments split at every quote. A cast operand routinely
|
|
598
|
+
* sits on the far side of a literal — `'2026-08-01'::date`,
|
|
599
|
+
* `(COALESCE(properties, '{}'))::text` — and the fragment view cuts that
|
|
600
|
+
* operand in half, which is why both read as "invalid PostgreSQL cast".
|
|
601
|
+
*/
|
|
602
|
+
function rewriteWithMaskedSqlLiterals(
|
|
603
|
+
sql: string,
|
|
604
|
+
rewrite: (code: string) => string,
|
|
605
|
+
): string {
|
|
606
|
+
if (sql.includes(SQL_LITERAL_PLACEHOLDER_PREFIX)) {
|
|
607
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
608
|
+
`an identifier reserved for query translation (${SQL_LITERAL_PLACEHOLDER_PREFIX}*)`,
|
|
609
|
+
`First-party BigQuery query cannot use identifiers starting with ${SQL_LITERAL_PLACEHOLDER_PREFIX}`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const literals: string[] = [];
|
|
613
|
+
let masked = "";
|
|
614
|
+
let cursor = 0;
|
|
615
|
+
while (cursor < sql.length) {
|
|
616
|
+
const literalStart = sql.indexOf("'", cursor);
|
|
617
|
+
if (literalStart === -1) {
|
|
618
|
+
masked += sql.slice(cursor);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
masked += sql.slice(cursor, literalStart);
|
|
622
|
+
let literalEnd = literalStart + 1;
|
|
623
|
+
while (literalEnd < sql.length) {
|
|
624
|
+
if (sql[literalEnd] !== "'") {
|
|
625
|
+
literalEnd++;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (sql[literalEnd + 1] === "'") {
|
|
629
|
+
literalEnd += 2;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
literalEnd++;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
masked += `${SQL_LITERAL_PLACEHOLDER_PREFIX}${literals.length}_`;
|
|
636
|
+
literals.push(sql.slice(literalStart, literalEnd));
|
|
637
|
+
cursor = literalEnd;
|
|
638
|
+
}
|
|
639
|
+
return rewrite(masked).replace(
|
|
640
|
+
new RegExp(`${SQL_LITERAL_PLACEHOLDER_PREFIX}(\\d+)_`, "g"),
|
|
641
|
+
(whole, index: string) => literals[Number(index)] ?? whole,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
575
645
|
function findMatchingSqlParen(sql: string, openIndex: number): number {
|
|
576
646
|
let depth = 0;
|
|
577
647
|
let inLiteral = false;
|
|
@@ -697,6 +767,46 @@ function coerceDateComparisonOperands(sql: string): string {
|
|
|
697
767
|
);
|
|
698
768
|
}
|
|
699
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Extent of the operand a `::` cast at `castIndex` applies to.
|
|
772
|
+
*
|
|
773
|
+
* The function-call case is the trap: stopping at the matching `(` leaves the
|
|
774
|
+
* function name outside the rewritten CAST, so `sum(x)::numeric` became
|
|
775
|
+
* `sumCAST((x) AS NUMERIC)` and BigQuery answered `Function not found: SUMCAST`.
|
|
776
|
+
*/
|
|
777
|
+
function postgresCastOperandBounds(
|
|
778
|
+
code: string,
|
|
779
|
+
castIndex: number,
|
|
780
|
+
): { start: number; end: number } {
|
|
781
|
+
let end = castIndex;
|
|
782
|
+
while (end > 0 && /\s/.test(code[end - 1] ?? "")) {
|
|
783
|
+
end--;
|
|
784
|
+
}
|
|
785
|
+
let start = end;
|
|
786
|
+
if (code[end - 1] === ")") {
|
|
787
|
+
let depth = 0;
|
|
788
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
789
|
+
if (code[index] === ")") depth++;
|
|
790
|
+
if (code[index] !== "(") continue;
|
|
791
|
+
depth--;
|
|
792
|
+
if (depth === 0) {
|
|
793
|
+
start = index;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
while (start > 0 && /[A-Za-z0-9_.$]/.test(code[start - 1] ?? "")) {
|
|
799
|
+
start--;
|
|
800
|
+
}
|
|
801
|
+
if (start === end) {
|
|
802
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
803
|
+
"a PostgreSQL cast with no readable operand",
|
|
804
|
+
"First-party BigQuery query has an invalid PostgreSQL cast",
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
return { start, end };
|
|
808
|
+
}
|
|
809
|
+
|
|
700
810
|
function replacePostgresCastsInCode(code: string): string {
|
|
701
811
|
const castType = new RegExp(
|
|
702
812
|
"::\\s*(date|timestamp|timestamptz|int|int2|int4|int8|integer|float|float4|float8|double\\s+precision|numeric|text|varchar|boolean|bool|json|jsonb)\\b",
|
|
@@ -728,40 +838,16 @@ function replacePostgresCastsInCode(code: string): string {
|
|
|
728
838
|
};
|
|
729
839
|
const targetType = mappedType[normalizedType];
|
|
730
840
|
if (!targetType) {
|
|
731
|
-
throw new
|
|
841
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
842
|
+
`a PostgreSQL ${normalizedType} cast`,
|
|
732
843
|
`First-party BigQuery query does not support PostgreSQL ${normalizedType} casts`,
|
|
733
844
|
);
|
|
734
845
|
}
|
|
735
846
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
let operandStart = operandEnd;
|
|
741
|
-
if (result[operandEnd - 1] === ")") {
|
|
742
|
-
let depth = 0;
|
|
743
|
-
for (let index = operandEnd - 1; index >= 0; index--) {
|
|
744
|
-
if (result[index] === ")") depth++;
|
|
745
|
-
if (result[index] !== "(") continue;
|
|
746
|
-
depth--;
|
|
747
|
-
if (depth === 0) {
|
|
748
|
-
operandStart = index;
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
} else {
|
|
753
|
-
while (
|
|
754
|
-
operandStart > 0 &&
|
|
755
|
-
/[A-Za-z0-9_.$]/.test(result[operandStart - 1] ?? "")
|
|
756
|
-
) {
|
|
757
|
-
operandStart--;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
if (operandStart === operandEnd) {
|
|
761
|
-
throw new Error(
|
|
762
|
-
"First-party BigQuery query has an invalid PostgreSQL cast",
|
|
763
|
-
);
|
|
764
|
-
}
|
|
847
|
+
const { start: operandStart, end: operandEnd } = postgresCastOperandBounds(
|
|
848
|
+
result,
|
|
849
|
+
match.index,
|
|
850
|
+
);
|
|
765
851
|
const operand = result.slice(operandStart, operandEnd).trim();
|
|
766
852
|
const castEnd = match.index + match[0].length;
|
|
767
853
|
result = `${result.slice(0, operandStart)}CAST(${operand} AS ${targetType})${result.slice(castEnd)}`;
|
|
@@ -810,12 +896,45 @@ function replaceBigQueryDateArithmetic(code: string): string {
|
|
|
810
896
|
return translated;
|
|
811
897
|
}
|
|
812
898
|
|
|
899
|
+
/**
|
|
900
|
+
* PostgreSQL truncates to the start of the ISO week (Monday); BigQuery's bare
|
|
901
|
+
* `WEEK` starts on Sunday, so the week mapping must name the weekday or the
|
|
902
|
+
* same query silently buckets differently on each backend.
|
|
903
|
+
*/
|
|
904
|
+
const BIGQUERY_DATE_TRUNC_PARTS: Record<string, string> = {
|
|
905
|
+
day: "DAY",
|
|
906
|
+
week: "WEEK(MONDAY)",
|
|
907
|
+
month: "MONTH",
|
|
908
|
+
quarter: "QUARTER",
|
|
909
|
+
year: "YEAR",
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* BigQuery JSONPath field names are always quoted here rather than only when
|
|
914
|
+
* they look unusual: an unquoted `$.$ai_model` is rejected outright, and an
|
|
915
|
+
* unquoted `$.page.title` silently reads a nested field the caller never asked
|
|
916
|
+
* for.
|
|
917
|
+
*/
|
|
918
|
+
function bigQueryJsonPath(key: string): string {
|
|
919
|
+
return `'$."${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"'`;
|
|
920
|
+
}
|
|
921
|
+
|
|
813
922
|
function translatePostgresJsonOperators(sql: string): string {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
923
|
+
const jsonExtract = /\s*::\s*jsonb?\s*->>\s*'([^']*)'/gi;
|
|
924
|
+
let result = sql;
|
|
925
|
+
let match = jsonExtract.exec(result);
|
|
926
|
+
while (match) {
|
|
927
|
+
const { start, end } = postgresCastOperandBounds(result, match.index);
|
|
928
|
+
const operand = result.slice(start, end).trim();
|
|
929
|
+
const replacement = `JSON_VALUE(${operand}, ${bigQueryJsonPath(match[1] ?? "")})`;
|
|
930
|
+
result =
|
|
931
|
+
result.slice(0, start) +
|
|
932
|
+
replacement +
|
|
933
|
+
result.slice(match.index + match[0].length);
|
|
934
|
+
jsonExtract.lastIndex = start + replacement.length;
|
|
935
|
+
match = jsonExtract.exec(result);
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
819
938
|
}
|
|
820
939
|
|
|
821
940
|
function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
@@ -837,16 +956,20 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
|
837
956
|
`INTERVAL ${amount} ${unit.replace(/s$/i, "").toUpperCase()}`,
|
|
838
957
|
);
|
|
839
958
|
translated = rewriteSqlFunctionCalls(translated, "date_trunc", (args) => {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
959
|
+
const unit = /^'([a-z]+)'$/i.exec(args[0] ?? "")?.[1]?.toLowerCase();
|
|
960
|
+
const datePart = unit ? BIGQUERY_DATE_TRUNC_PARTS[unit] : undefined;
|
|
961
|
+
if (args.length !== 2 || !datePart) {
|
|
962
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
963
|
+
`date_trunc(${args[0] ?? "?"}, ...)`,
|
|
964
|
+
`First-party BigQuery query supports PostgreSQL date_trunc with ${Object.keys(BIGQUERY_DATE_TRUNC_PARTS).join(", ")}`,
|
|
843
965
|
);
|
|
844
966
|
}
|
|
845
|
-
return `DATE_TRUNC(CAST(${translatePostgresDateExpression(args[1] ?? "")} AS DATE),
|
|
967
|
+
return `DATE_TRUNC(CAST(${translatePostgresDateExpression(args[1] ?? "")} AS DATE), ${datePart})`;
|
|
846
968
|
});
|
|
847
969
|
translated = rewriteSqlFunctionCalls(translated, "to_char", (args) => {
|
|
848
970
|
if (args.length !== 2 || !/^'YYYY-MM-DD'$/i.test(args[1] ?? "")) {
|
|
849
|
-
throw new
|
|
971
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
972
|
+
`to_char(..., ${args[1] ?? "?"})`,
|
|
850
973
|
"First-party BigQuery query only supports PostgreSQL to_char(..., 'YYYY-MM-DD') expressions",
|
|
851
974
|
);
|
|
852
975
|
}
|
|
@@ -854,7 +977,10 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
|
854
977
|
});
|
|
855
978
|
translated = rewriteSqlFunctionCalls(translated, "chr", (args) => {
|
|
856
979
|
if (args.length !== 1) {
|
|
857
|
-
throw new
|
|
980
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
981
|
+
"a multi-argument chr(...) call",
|
|
982
|
+
"First-party BigQuery query has an invalid chr call",
|
|
983
|
+
);
|
|
858
984
|
}
|
|
859
985
|
return `CHR(${args[0]})`;
|
|
860
986
|
});
|
|
@@ -864,14 +990,15 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
|
864
990
|
translated = rewriteSqlFunctionCalls(translated, "split_part", (args) => {
|
|
865
991
|
const index = Number(args[2]);
|
|
866
992
|
if (args.length !== 3 || !Number.isInteger(index) || index < 1) {
|
|
867
|
-
throw new
|
|
993
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
994
|
+
"split_part(...) with a non-literal or non-positive index",
|
|
868
995
|
"First-party BigQuery query only supports split_part with a positive integer index",
|
|
869
996
|
);
|
|
870
997
|
}
|
|
871
998
|
return `SPLIT(${args[0]}, ${args[1]})[SAFE_OFFSET(${index - 1})]`;
|
|
872
999
|
});
|
|
873
1000
|
}
|
|
874
|
-
translated =
|
|
1001
|
+
translated = rewriteWithMaskedSqlLiterals(translated, (code) => {
|
|
875
1002
|
let rewritten = replacePostgresCastsInCode(code);
|
|
876
1003
|
rewritten = rewritten
|
|
877
1004
|
.replace(/\bnow\s*\(\s*\)/gi, "CURRENT_TIMESTAMP()")
|
|
@@ -893,10 +1020,16 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
|
893
1020
|
[/\bINTERVAL\s*'/i, "PostgreSQL interval literals"],
|
|
894
1021
|
[/\bAT\s+TIME\s+ZONE\b/i, "AT TIME ZONE"],
|
|
895
1022
|
[/\bFILTER\s*\(\s*WHERE\b/i, "FILTER (WHERE ...)"],
|
|
1023
|
+
[/\bDISTINCT\s+ON\b/i, "SELECT DISTINCT ON"],
|
|
1024
|
+
// Anything the JSON translation above could not consume. BigQuery has no
|
|
1025
|
+
// such operators, so leaving it through buys a provider 400 instead of a
|
|
1026
|
+
// rendered explanation.
|
|
1027
|
+
[/->>|->|#>>|@>/, "PostgreSQL JSON operators"],
|
|
896
1028
|
];
|
|
897
1029
|
const incompatible = unsupported.find(([pattern]) => pattern.test(code));
|
|
898
1030
|
if (incompatible) {
|
|
899
|
-
throw new
|
|
1031
|
+
throw new FirstPartyAnalyticsUnsupportedSqlError(
|
|
1032
|
+
incompatible[1],
|
|
900
1033
|
`First-party analytics query uses unsupported PostgreSQL syntax (${incompatible[1]}) after BigQuery translation`,
|
|
901
1034
|
);
|
|
902
1035
|
}
|
|
@@ -977,6 +1110,16 @@ function addPartitionPrunedEventDeduplication(
|
|
|
977
1110
|
return result;
|
|
978
1111
|
}
|
|
979
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Throws `FirstPartyAnalyticsUnsupportedSqlError` when this SQL has no BigQuery
|
|
1115
|
+
* translation. Save-time validation runs on the panel's own (unscoped) SQL,
|
|
1116
|
+
* which is a subset of what the read path translates, so a pass here cannot
|
|
1117
|
+
* pass a construct through that the read path would then reject.
|
|
1118
|
+
*/
|
|
1119
|
+
export function assertFirstPartyAnalyticsBigQuerySql(sql: string): void {
|
|
1120
|
+
translateFirstPartyAnalyticsBigQuerySql(sql);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
980
1123
|
export function renderFirstPartyAnalyticsBigQuerySql(
|
|
981
1124
|
scopedSql: string,
|
|
982
1125
|
args: Array<string | null>,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type DerivedExceptionFields,
|
|
11
11
|
} from "./error-capture.js";
|
|
12
12
|
import {
|
|
13
|
+
assertFirstPartyAnalyticsBigQuerySql,
|
|
14
|
+
type FirstPartyAnalyticsSink,
|
|
13
15
|
getFirstPartyAnalyticsBackend,
|
|
14
16
|
getFirstPartyAnalyticsTable,
|
|
15
17
|
insertFirstPartyAnalyticsRows,
|
|
@@ -1088,6 +1090,44 @@ function inferSchema(rows: Record<string, unknown>[]): {
|
|
|
1088
1090
|
}));
|
|
1089
1091
|
}
|
|
1090
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* Which store this query actually runs against under a given sink. Save-time
|
|
1095
|
+
* validation and the read path both route through here: a panel validated for
|
|
1096
|
+
* one store and executed against the other is the whole bug class.
|
|
1097
|
+
*/
|
|
1098
|
+
function firstPartyAnalyticsQueryTarget(
|
|
1099
|
+
sql: string,
|
|
1100
|
+
sink: FirstPartyAnalyticsSink,
|
|
1101
|
+
): "sql-store" | "bigquery" {
|
|
1102
|
+
if (sink !== "bigquery") return "sql-store";
|
|
1103
|
+
const usesSessionRecordings = /\bsession_recordings\b/i.test(sql);
|
|
1104
|
+
const usesEventTables =
|
|
1105
|
+
/\banalytics_events\b|\banalytics_event_daily_rollups\b|\banalytics_user_days\b/i.test(
|
|
1106
|
+
sql,
|
|
1107
|
+
);
|
|
1108
|
+
if (usesSessionRecordings && usesEventTables) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
"Cross-backend joins are not supported; query first-party event tables in BigQuery and session_recordings in the Analytics SQL store separately.",
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
return usesSessionRecordings ? "sql-store" : "bigquery";
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Save-time counterpart to `queryFirstPartyAnalytics`. `sink` is a mutable
|
|
1118
|
+
* per-scope setting, so generic PostgreSQL validation alone accepts panels the
|
|
1119
|
+
* live backend cannot execute.
|
|
1120
|
+
*/
|
|
1121
|
+
export async function validateFirstPartyAnalyticsSqlForScope(
|
|
1122
|
+
sql: string,
|
|
1123
|
+
scope: AnalyticsScope,
|
|
1124
|
+
): Promise<void> {
|
|
1125
|
+
validateFirstPartyAnalyticsSql(sql);
|
|
1126
|
+
const backend = await getFirstPartyAnalyticsBackend(scope);
|
|
1127
|
+
if (firstPartyAnalyticsQueryTarget(sql, backend.sink) !== "bigquery") return;
|
|
1128
|
+
assertFirstPartyAnalyticsBigQuerySql(sql);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1091
1131
|
export async function queryFirstPartyAnalytics(
|
|
1092
1132
|
sql: string,
|
|
1093
1133
|
scope: AnalyticsScope,
|
|
@@ -1095,22 +1135,10 @@ export async function queryFirstPartyAnalytics(
|
|
|
1095
1135
|
): Promise<AnalyticsQueryResult> {
|
|
1096
1136
|
validateFirstPartyAnalyticsSql(sql);
|
|
1097
1137
|
const backend = await getFirstPartyAnalyticsBackend(scope);
|
|
1098
|
-
if (backend.sink === "bigquery") {
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
sql,
|
|
1103
|
-
);
|
|
1104
|
-
if (usesSessionRecordings && usesEventTables) {
|
|
1105
|
-
throw new Error(
|
|
1106
|
-
"Cross-backend joins are not supported; query first-party event tables in BigQuery and session_recordings in the Analytics SQL store separately.",
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
if (!usesSessionRecordings) {
|
|
1110
|
-
const table = await getFirstPartyAnalyticsTable(backend.table);
|
|
1111
|
-
const scoped = scopedAnalyticsSql(sql, scope);
|
|
1112
|
-
return queryFirstPartyAnalyticsInBigQuery(scoped.sql, scoped.args, table);
|
|
1113
|
-
}
|
|
1138
|
+
if (firstPartyAnalyticsQueryTarget(sql, backend.sink) === "bigquery") {
|
|
1139
|
+
const table = await getFirstPartyAnalyticsTable(backend.table);
|
|
1140
|
+
const scoped = scopedAnalyticsSql(sql, scope);
|
|
1141
|
+
return queryFirstPartyAnalyticsInBigQuery(scoped.sql, scoped.args, table);
|
|
1114
1142
|
}
|
|
1115
1143
|
const scoped = scopedAnalyticsSql(sql, scope);
|
|
1116
1144
|
const wrappedSql = `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`;
|
|
@@ -655,6 +655,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
|
|
|
655
655
|
const explicitErrMsg = event.error || event.message || event.detail;
|
|
656
656
|
const errMsg = explicitErrMsg ??
|
|
657
657
|
`Gateway error (no detail; raw event: ${JSON.stringify(event)})`;
|
|
658
|
+
const gatewayRequestId = typeof event.requestId === "string" ? event.requestId : undefined;
|
|
658
659
|
const gatewayErrCode = event.errorCode ?? event.code;
|
|
659
660
|
// The gateway already authenticated this request before streaming,
|
|
660
661
|
// so a bare "Unauthorized" here means the account cannot use this
|
|
@@ -684,7 +685,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
|
|
|
684
685
|
// would otherwise do it downstream on the visitor line and
|
|
685
686
|
// record `unknown` on the credits lane alone.
|
|
686
687
|
classifyTerminalErrorCode(String(errMsg))));
|
|
687
|
-
console.error(`[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} error=${errMsg}`);
|
|
688
|
+
console.error(`[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} requestId=${gatewayRequestId ?? "(none)"} error=${errMsg}`);
|
|
688
689
|
if (isCredentialAuthError) {
|
|
689
690
|
await recordBuilderGatewayAuthFailure({
|
|
690
691
|
code: typeof gatewayErrCode === "string" ? gatewayErrCode : errCode,
|
|
@@ -699,9 +700,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
|
|
|
699
700
|
// it's thrown, but without these tags.
|
|
700
701
|
if (!explicitErrMsg) {
|
|
701
702
|
captureBuilderGatewayNoDetailError({
|
|
702
|
-
requestId:
|
|
703
|
-
? event.requestId
|
|
704
|
-
: undefined,
|
|
703
|
+
requestId: gatewayRequestId,
|
|
705
704
|
model,
|
|
706
705
|
gatewayUrl: captureContext.gatewayUrl,
|
|
707
706
|
rawEvent: event,
|
|
@@ -716,6 +715,10 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
|
|
|
716
715
|
...(isTransientGatewayFailure(String(errMsg))
|
|
717
716
|
? { providerRetryable: true }
|
|
718
717
|
: {}),
|
|
718
|
+
// requestId rides the stop event whether or not the gateway sent a
|
|
719
|
+
// message: a message like "...ERROR ID: <hex>" is as opaque as no
|
|
720
|
+
// message at all, and this is the only key that reaches upstream.
|
|
721
|
+
...(gatewayRequestId ? { requestId: gatewayRequestId } : {}),
|
|
719
722
|
});
|
|
720
723
|
}
|
|
721
724
|
else if (reason === "end_turn" ||
|
|
@@ -26,6 +26,8 @@ export declare class EngineError extends Error {
|
|
|
26
26
|
readonly statusCode?: number;
|
|
27
27
|
/** Whether the provider explicitly marked this error as retryable. */
|
|
28
28
|
readonly providerRetryable?: boolean;
|
|
29
|
+
/** Upstream request id, when the provider/gateway supplied one. */
|
|
30
|
+
readonly requestId?: string;
|
|
29
31
|
/**
|
|
30
32
|
* Whether the request exceeded the model's context window. Set by engines that
|
|
31
33
|
* classified the provider's own reply, because the delivered message may not
|
|
@@ -39,6 +41,7 @@ export declare class EngineError extends Error {
|
|
|
39
41
|
upgradeUrl?: string;
|
|
40
42
|
statusCode?: number;
|
|
41
43
|
providerRetryable?: boolean;
|
|
44
|
+
requestId?: string;
|
|
42
45
|
contextOverflow?: boolean;
|
|
43
46
|
});
|
|
44
47
|
}
|
|
@@ -196,6 +199,13 @@ export type EngineEvent = {
|
|
|
196
199
|
* should retry even if status code / message patterns don't match.
|
|
197
200
|
*/
|
|
198
201
|
providerRetryable?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Upstream request id, when the provider/gateway supplies one. This is
|
|
204
|
+
* the only key that ties a user-facing error back to the upstream log,
|
|
205
|
+
* so it must survive to the capture even when the error also carries a
|
|
206
|
+
* message — an opaque message is not a diagnostic.
|
|
207
|
+
*/
|
|
208
|
+
requestId?: string;
|
|
199
209
|
/**
|
|
200
210
|
* The request exceeded the model's context window. Carried structurally
|
|
201
211
|
* for the same reason as `providerRetryable`: `error` is visitor copy on a
|
|
@@ -25,6 +25,8 @@ export class EngineError extends Error {
|
|
|
25
25
|
statusCode;
|
|
26
26
|
/** Whether the provider explicitly marked this error as retryable. */
|
|
27
27
|
providerRetryable;
|
|
28
|
+
/** Upstream request id, when the provider/gateway supplied one. */
|
|
29
|
+
requestId;
|
|
28
30
|
/**
|
|
29
31
|
* Whether the request exceeded the model's context window. Set by engines that
|
|
30
32
|
* classified the provider's own reply, because the delivered message may not
|
|
@@ -40,6 +42,7 @@ export class EngineError extends Error {
|
|
|
40
42
|
this.upgradeUrl = opts?.upgradeUrl;
|
|
41
43
|
this.statusCode = opts?.statusCode;
|
|
42
44
|
this.providerRetryable = opts?.providerRetryable;
|
|
45
|
+
this.requestId = opts?.requestId;
|
|
43
46
|
this.contextOverflow = opts?.contextOverflow;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -1060,6 +1060,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1060
1060
|
let pendingTerminalEvent = null;
|
|
1061
1061
|
const captureRunError = (error, phase) => {
|
|
1062
1062
|
const errorCode = getRunErrorCode(error);
|
|
1063
|
+
// A gateway error often arrives as one opaque user-facing sentence, so the
|
|
1064
|
+
// structured fields EngineError already carries are the whole diagnostic.
|
|
1065
|
+
// Dropping them here left operators with an error id and nothing to join on.
|
|
1066
|
+
const engineError = error instanceof EngineError ? error : null;
|
|
1063
1067
|
captureError(error, {
|
|
1064
1068
|
route: "/_agent-native/agent-chat",
|
|
1065
1069
|
aiTraceId: runId,
|
|
@@ -1070,6 +1074,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
|
|
|
1070
1074
|
softTimedOut: softTimedOut ? "true" : "false",
|
|
1071
1075
|
abortReason: run.abortReason,
|
|
1072
1076
|
errorCode,
|
|
1077
|
+
gatewayRequestId: engineError?.requestId,
|
|
1078
|
+
statusCode: engineError?.statusCode != null
|
|
1079
|
+
? String(engineError.statusCode)
|
|
1080
|
+
: undefined,
|
|
1073
1081
|
},
|
|
1074
1082
|
extra: {
|
|
1075
1083
|
runId,
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
44
45
|
summary: import("./types.js").TraceSummary;
|
|
45
46
|
spans: import("./types.js").TraceSpan[];
|
|
46
47
|
id?: undefined;
|
|
47
|
-
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
+
error?: undefined;
|
|
50
51
|
summary?: undefined;
|
|
51
52
|
spans?: undefined;
|
|
52
53
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
65
|
id?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
|
-
error?: undefined;
|
|
67
67
|
}>>;
|
|
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
-
id?: undefined;
|
|
79
78
|
message?: undefined;
|
|
79
|
+
id?: undefined;
|
|
80
80
|
deleted?: undefined;
|
|
81
81
|
providers: {
|
|
82
82
|
id: string;
|
|
@@ -93,9 +93,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
93
93
|
registered?: undefined;
|
|
94
94
|
label?: undefined;
|
|
95
95
|
} | {
|
|
96
|
-
id?: undefined;
|
|
97
96
|
message?: undefined;
|
|
98
97
|
count?: undefined;
|
|
98
|
+
id?: undefined;
|
|
99
99
|
deleted?: undefined;
|
|
100
100
|
providers?: undefined;
|
|
101
101
|
found: boolean;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
ok?: undefined;
|
|
52
51
|
error: string;
|
|
52
|
+
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
@@ -11,6 +11,7 @@ import { runAutomationSchedulerHealthMigrations } from "../jobs/scheduler-health
|
|
|
11
11
|
import { OAUTH_TOKEN_MIGRATIONS, OAUTH_TOKEN_MIGRATIONS_TABLE, } from "../oauth-tokens/migrations.js";
|
|
12
12
|
import { ORG_MIGRATIONS } from "../org/migrations.js";
|
|
13
13
|
import { USAGE_ALERT_MIGRATIONS, USAGE_ALERT_MIGRATIONS_TABLE, } from "../usage/migrations.js";
|
|
14
|
+
import { WORKSPACE_CONNECTIONS_MIGRATIONS, WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE, } from "../workspace-connections/migrations.js";
|
|
14
15
|
import { runBetterAuthMigrations } from "./better-auth-migrations.js";
|
|
15
16
|
import { IDENTITY_SSO_MIGRATIONS } from "./identity-sso-migrations.js";
|
|
16
17
|
/**
|
|
@@ -53,6 +54,9 @@ export async function runFrameworkReleaseMigrations(nitroApp) {
|
|
|
53
54
|
await runMigrations(OBSERVATIONAL_MEMORY_MIGRATIONS, {
|
|
54
55
|
table: "_observational_memory_migrations",
|
|
55
56
|
})(nitroApp);
|
|
57
|
+
await runMigrations(WORKSPACE_CONNECTIONS_MIGRATIONS, {
|
|
58
|
+
table: WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE,
|
|
59
|
+
})(nitroApp);
|
|
56
60
|
await runAutomationRunMigrations(nitroApp);
|
|
57
61
|
await runAutomationSchedulerHealthMigrations(nitroApp);
|
|
58
62
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MigrationEntry } from "../db/migrations.js";
|
|
2
|
+
export declare const WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE = "_workspace_connections_migrations";
|
|
3
|
+
/**
|
|
4
|
+
* Deploy-time schema for workspace connections, grants, and user groups.
|
|
5
|
+
*
|
|
6
|
+
* These three tables were shipped with only their runtime `ensureTable`
|
|
7
|
+
* helpers in `store.ts` / `groups.ts`. That is enough locally and on a
|
|
8
|
+
* long-lived server, but `schemaEnsureDisabled()` makes every probe report
|
|
9
|
+
* "present" on a production serverless runtime, so the ensure path issues no
|
|
10
|
+
* DDL there at all. A table with no entry here therefore never gets created in
|
|
11
|
+
* production, and the first read fails with `relation ... does not exist` —
|
|
12
|
+
* which is exactly what `workspace_user_groups` did from the day after it
|
|
13
|
+
* shipped. Runtime ensure covers dev; this list is the production contract.
|
|
14
|
+
*
|
|
15
|
+
* `created_at` / `updated_at` must be BIGINT on Postgres: they store epoch
|
|
16
|
+
* milliseconds, which overflow int4.
|
|
17
|
+
*/
|
|
18
|
+
export declare const WORKSPACE_CONNECTIONS_MIGRATIONS: MigrationEntry[];
|