@agent-native/core 0.98.1 → 0.98.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/README.md +2 -2
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +1 -1
- package/corpus/core/docs/content/pr-visual-recap.mdx +50 -1
- package/corpus/core/docs/content/template-plan.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/plan-local.ts +43 -11
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -9
- package/corpus/core/src/cli/recap.ts +356 -4
- package/corpus/core/src/cli/skills-content/local-files.ts +17 -12
- package/corpus/core/src/client/analytics.ts +96 -14
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/client/session-replay.ts +10 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +4 -28
- package/corpus/core/src/client/url-scrub.ts +13 -0
- package/corpus/core/src/client/use-demo-mode-status.ts +44 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -4
- package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +156 -0
- package/corpus/templates/analytics/changelog/2026-07-12-daily-dashboard-emails-reliably-include-the-complete-dashboa.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-charts-trend-upward.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +17 -33
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
- package/corpus/templates/clips/changelog/2026-07-11-clips-no-longer-stay-stuck-on-preparing-clip-when-they-are-r.md +6 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +2 -2
- package/corpus/templates/content/changelog/2026-07-12-the-left-sidebar-stays-fixed-horizontally-while-scrolling-on.md +6 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/local-files.md +17 -12
- package/corpus/templates/plan/.agents/skills/visual-recap/references/local-files.md +17 -12
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +4 -4
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/plan/app/lib/plan-local-bridge.ts +41 -0
- package/corpus/templates/plan/app/lib/plan-tracking.ts +3 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -9
- package/corpus/templates/plan/app/root.tsx +13 -3
- package/corpus/templates/plan/changelog/2026-07-12-local-plan-source-now-stays-on-device.md +6 -0
- package/dist/cli/plan-local.d.ts +6 -6
- package/dist/cli/plan-local.d.ts.map +1 -1
- package/dist/cli/plan-local.js +36 -7
- package/dist/cli/plan-local.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +2 -8
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +2 -8
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +22 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +286 -2
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills-content/local-files.d.ts +1 -1
- package/dist/cli/skills-content/local-files.d.ts.map +1 -1
- package/dist/cli/skills-content/local-files.js +17 -12
- package/dist/cli/skills-content/local-files.js.map +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +77 -13
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +2 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +7 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +4 -23
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/url-scrub.d.ts.map +1 -1
- package/dist/client/url-scrub.js +14 -0
- package/dist/client/url-scrub.js.map +1 -1
- package/dist/client/use-demo-mode-status.d.ts +12 -0
- package/dist/client/use-demo-mode-status.d.ts.map +1 -0
- package/dist/client/use-demo-mode-status.js +30 -0
- package/dist/client/use-demo-mode-status.js.map +1 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ar-SA/template-plan.mdx +1 -1
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/de-DE/template-plan.mdx +1 -1
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/es-ES/template-plan.mdx +1 -1
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/fr-FR/template-plan.mdx +1 -1
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/hi-IN/template-plan.mdx +1 -1
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ja-JP/template-plan.mdx +1 -1
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ko-KR/template-plan.mdx +1 -1
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/pt-BR/template-plan.mdx +1 -1
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/zh-CN/template-plan.mdx +1 -1
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/zh-TW/template-plan.mdx +1 -1
- package/docs/content/pr-visual-recap.mdx +50 -1
- package/docs/content/template-plan.mdx +1 -1
- package/package.json +1 -1
|
@@ -89,6 +89,13 @@ export type ConfigureTrackingOptions = {
|
|
|
89
89
|
/** First-party analytics track endpoint. */
|
|
90
90
|
endpoint?: string;
|
|
91
91
|
getDefaultProps?: GetDefaultProps;
|
|
92
|
+
/**
|
|
93
|
+
* Disable content-capturing analytics such as interaction autocapture and
|
|
94
|
+
* session replay while retaining pageviews, explicit events, and Sentry.
|
|
95
|
+
*/
|
|
96
|
+
contentCapture?: boolean;
|
|
97
|
+
/** Resolve content capture synchronously for each browser pathname. */
|
|
98
|
+
contentCaptureForPath?: (pathname: string) => boolean;
|
|
92
99
|
sessionReplay?: boolean | SessionReplayOptions;
|
|
93
100
|
/**
|
|
94
101
|
* First-party, Sentry-style error capture. Auto-captures uncaught errors
|
|
@@ -132,6 +139,8 @@ let _errorCaptureDisposer: (() => void) | null = null;
|
|
|
132
139
|
let _sessionReplayModuleForCapture:
|
|
133
140
|
| typeof import("./session-replay.js")
|
|
134
141
|
| null = null;
|
|
142
|
+
let _trackingContentCaptureEnabled = true;
|
|
143
|
+
let _contentCaptureForPath: ((pathname: string) => boolean) | null = null;
|
|
135
144
|
// Buffer for setSentryUser calls made before Sentry has initialized.
|
|
136
145
|
// `undefined` means "no pending update"; `null` means "pending clear".
|
|
137
146
|
let _pendingSentryUser: SentryUser | null | undefined = undefined;
|
|
@@ -575,7 +584,10 @@ function ensureAmplitude(): boolean {
|
|
|
575
584
|
const key = (import.meta.env as Record<string, string | undefined>)
|
|
576
585
|
?.VITE_AMPLITUDE_API_KEY;
|
|
577
586
|
if (!key) return false;
|
|
578
|
-
|
|
587
|
+
// Standard pageviews and explicit events are emitted below. Keep SDK-level
|
|
588
|
+
// DOM/network autocapture off so rendered user content is never collected as
|
|
589
|
+
// an implicit analytics side effect.
|
|
590
|
+
amplitude.init(key, { autocapture: false });
|
|
579
591
|
_amplitudeInitialized = true;
|
|
580
592
|
return true;
|
|
581
593
|
}
|
|
@@ -896,7 +908,11 @@ export function setSentryUser(
|
|
|
896
908
|
clearTrackingIdentity();
|
|
897
909
|
}
|
|
898
910
|
_trackingIdentityResolved = true;
|
|
899
|
-
if (
|
|
911
|
+
if (
|
|
912
|
+
shouldRetryReplay &&
|
|
913
|
+
_trackingContentCaptureEnabled &&
|
|
914
|
+
_sessionReplayOptions?.requireSignedInUser
|
|
915
|
+
) {
|
|
900
916
|
void startConfiguredSessionReplay(_sessionReplayOptions);
|
|
901
917
|
}
|
|
902
918
|
if (_sentryInitialized) {
|
|
@@ -1004,6 +1020,11 @@ export function configureTracking(options: ConfigureTrackingOptions): void {
|
|
|
1004
1020
|
if (options.getDefaultProps) {
|
|
1005
1021
|
_getDefaultProps = options.getDefaultProps;
|
|
1006
1022
|
}
|
|
1023
|
+
_contentCaptureForPath = options.contentCaptureForPath ?? null;
|
|
1024
|
+
_trackingContentCaptureEnabled =
|
|
1025
|
+
_contentCaptureForPath && typeof window !== "undefined"
|
|
1026
|
+
? _contentCaptureForPath(window.location.pathname)
|
|
1027
|
+
: options.contentCapture !== false;
|
|
1007
1028
|
if (typeof window !== "undefined") {
|
|
1008
1029
|
ensureSentry();
|
|
1009
1030
|
ensureAmplitude();
|
|
@@ -1011,14 +1032,37 @@ export function configureTracking(options: ConfigureTrackingOptions): void {
|
|
|
1011
1032
|
installLlmConnectionRefresh();
|
|
1012
1033
|
installTrackingAuthSessionRefresh();
|
|
1013
1034
|
installPageviewTracking();
|
|
1014
|
-
maybeInstallSessionReplay(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1035
|
+
maybeInstallSessionReplay(
|
|
1036
|
+
options.sessionReplay,
|
|
1037
|
+
{
|
|
1038
|
+
endpoint: options.endpoint,
|
|
1039
|
+
publicKey,
|
|
1040
|
+
},
|
|
1041
|
+
_trackingContentCaptureEnabled,
|
|
1042
|
+
);
|
|
1018
1043
|
maybeInstallErrorCapture(options.errorCapture);
|
|
1019
1044
|
}
|
|
1020
1045
|
}
|
|
1021
1046
|
|
|
1047
|
+
export function setTrackingContentCaptureEnabled(enabled: boolean): void {
|
|
1048
|
+
if (_trackingContentCaptureEnabled === enabled) return;
|
|
1049
|
+
_trackingContentCaptureEnabled = enabled;
|
|
1050
|
+
if (enabled) {
|
|
1051
|
+
if (_sessionReplayOptions) {
|
|
1052
|
+
void startConfiguredSessionReplay(_sessionReplayOptions);
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
void stopSessionReplay("content-capture-disabled");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function syncTrackingContentCaptureForLocation(): void {
|
|
1060
|
+
if (!_contentCaptureForPath) return;
|
|
1061
|
+
setTrackingContentCaptureEnabled(
|
|
1062
|
+
_contentCaptureForPath(window.location.pathname),
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1022
1066
|
/**
|
|
1023
1067
|
* Lazily load the session-replay module so error capture can read the active
|
|
1024
1068
|
* replay id and surface manual captures on the replay timeline without a
|
|
@@ -1254,11 +1298,12 @@ function replayEndpointFromTrackingEndpoint(value: string): string | undefined {
|
|
|
1254
1298
|
function maybeInstallSessionReplay(
|
|
1255
1299
|
config: boolean | SessionReplayOptions | undefined,
|
|
1256
1300
|
tracking?: { endpoint?: string; publicKey?: string },
|
|
1301
|
+
start = true,
|
|
1257
1302
|
): void {
|
|
1258
1303
|
if (typeof window === "undefined") return;
|
|
1259
1304
|
const options = configuredSessionReplayOptions(config, tracking);
|
|
1260
|
-
if (!options) return;
|
|
1261
1305
|
_sessionReplayOptions = options;
|
|
1306
|
+
if (!options || !start) return;
|
|
1262
1307
|
void startConfiguredSessionReplay(options);
|
|
1263
1308
|
}
|
|
1264
1309
|
|
|
@@ -1284,11 +1329,24 @@ async function startConfiguredSessionReplay(
|
|
|
1284
1329
|
): Promise<SessionReplayStartResult | null> {
|
|
1285
1330
|
if (_sessionReplayStartPromise) return _sessionReplayStartPromise;
|
|
1286
1331
|
_sessionReplayStartPromise = (async () => {
|
|
1332
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1333
|
+
return { started: false, reason: "disabled" as const };
|
|
1334
|
+
}
|
|
1287
1335
|
if (!(await waitForSessionReplayAuthIfRequired(options))) {
|
|
1288
1336
|
return { started: false, reason: "missing-user-id" as const };
|
|
1289
1337
|
}
|
|
1338
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1339
|
+
return { started: false, reason: "disabled" as const };
|
|
1340
|
+
}
|
|
1290
1341
|
const mod = await import("./session-replay.js");
|
|
1291
|
-
|
|
1342
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1343
|
+
return { started: false, reason: "disabled" as const };
|
|
1344
|
+
}
|
|
1345
|
+
return mod.startSessionReplay({
|
|
1346
|
+
...options,
|
|
1347
|
+
shouldStart: () =>
|
|
1348
|
+
_trackingContentCaptureEnabled && (options.shouldStart?.() ?? true),
|
|
1349
|
+
});
|
|
1292
1350
|
})()
|
|
1293
1351
|
.catch(() => ({ started: false, reason: "import-failed" as const }))
|
|
1294
1352
|
.finally(() => {
|
|
@@ -1300,23 +1358,37 @@ async function startConfiguredSessionReplay(
|
|
|
1300
1358
|
export async function startSessionReplay(
|
|
1301
1359
|
options: SessionReplayOptions = {},
|
|
1302
1360
|
): Promise<SessionReplayStartResult> {
|
|
1361
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1362
|
+
return { started: false, reason: "disabled" };
|
|
1363
|
+
}
|
|
1303
1364
|
const configured = configuredSessionReplayOptions(options) ?? options;
|
|
1304
1365
|
if (!(await waitForSessionReplayAuthIfRequired(configured))) {
|
|
1305
1366
|
return { started: false, reason: "missing-user-id" };
|
|
1306
1367
|
}
|
|
1307
1368
|
const mod = await import("./session-replay.js");
|
|
1308
|
-
return mod.startSessionReplay(
|
|
1369
|
+
return mod.startSessionReplay({
|
|
1370
|
+
...configured,
|
|
1371
|
+
shouldStart: () =>
|
|
1372
|
+
_trackingContentCaptureEnabled && (configured.shouldStart?.() ?? true),
|
|
1373
|
+
});
|
|
1309
1374
|
}
|
|
1310
1375
|
|
|
1311
1376
|
export async function maybeStartSessionReplay(
|
|
1312
1377
|
options: SessionReplayOptions = {},
|
|
1313
1378
|
): Promise<SessionReplayStartResult> {
|
|
1379
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1380
|
+
return { started: false, reason: "disabled" };
|
|
1381
|
+
}
|
|
1314
1382
|
const configured = configuredSessionReplayOptions(options) ?? options;
|
|
1315
1383
|
if (!(await waitForSessionReplayAuthIfRequired(configured))) {
|
|
1316
1384
|
return { started: false, reason: "missing-user-id" };
|
|
1317
1385
|
}
|
|
1318
1386
|
const mod = await import("./session-replay.js");
|
|
1319
|
-
return mod.maybeStartSessionReplay(
|
|
1387
|
+
return mod.maybeStartSessionReplay({
|
|
1388
|
+
...configured,
|
|
1389
|
+
shouldStart: () =>
|
|
1390
|
+
_trackingContentCaptureEnabled && (configured.shouldStart?.() ?? true),
|
|
1391
|
+
});
|
|
1320
1392
|
}
|
|
1321
1393
|
|
|
1322
1394
|
export async function stopSessionReplay(reason = "manual"): Promise<void> {
|
|
@@ -1371,15 +1443,17 @@ function pageviewKey(): string {
|
|
|
1371
1443
|
|
|
1372
1444
|
function pageviewProperties(reason: string): Record<string, unknown> {
|
|
1373
1445
|
const properties: Record<string, unknown> = {
|
|
1374
|
-
url:
|
|
1446
|
+
url: !_trackingContentCaptureEnabled
|
|
1447
|
+
? window.location.origin + window.location.pathname
|
|
1448
|
+
: scrubUrl(window.location.href),
|
|
1375
1449
|
path: window.location.pathname,
|
|
1376
1450
|
hostname: window.location.hostname,
|
|
1377
1451
|
navigation_type: reason,
|
|
1378
1452
|
};
|
|
1379
|
-
if (window.location.search) {
|
|
1453
|
+
if (_trackingContentCaptureEnabled && window.location.search) {
|
|
1380
1454
|
properties.search = scrubUrl(window.location.search);
|
|
1381
1455
|
}
|
|
1382
|
-
if (typeof document !== "undefined") {
|
|
1456
|
+
if (_trackingContentCaptureEnabled && typeof document !== "undefined") {
|
|
1383
1457
|
if (document.referrer) {
|
|
1384
1458
|
properties.referrer = scrubUrl(document.referrer);
|
|
1385
1459
|
}
|
|
@@ -1400,6 +1474,9 @@ function emitPageview(reason: string): void {
|
|
|
1400
1474
|
}
|
|
1401
1475
|
|
|
1402
1476
|
function schedulePageview(reason: string): void {
|
|
1477
|
+
if (!_trackingContentCaptureEnabled) {
|
|
1478
|
+
void stopSessionReplay("local-plan-privacy");
|
|
1479
|
+
}
|
|
1403
1480
|
const run = () => emitPageview(reason);
|
|
1404
1481
|
const pendingStartupContext: Array<Promise<void>> = [];
|
|
1405
1482
|
if (_llmConnectionRefresh && !_llmConnectionStatus) {
|
|
@@ -1436,17 +1513,22 @@ function installPageviewTracking(): void {
|
|
|
1436
1513
|
|
|
1437
1514
|
window.history.pushState = function pushState(...args) {
|
|
1438
1515
|
const result = originalPushState.apply(this, args);
|
|
1516
|
+
syncTrackingContentCaptureForLocation();
|
|
1439
1517
|
schedulePageview("pushState");
|
|
1440
1518
|
return result;
|
|
1441
1519
|
};
|
|
1442
1520
|
|
|
1443
1521
|
window.history.replaceState = function replaceState(...args) {
|
|
1444
1522
|
const result = originalReplaceState.apply(this, args);
|
|
1523
|
+
syncTrackingContentCaptureForLocation();
|
|
1445
1524
|
schedulePageview("replaceState");
|
|
1446
1525
|
return result;
|
|
1447
1526
|
};
|
|
1448
1527
|
|
|
1449
|
-
window.addEventListener("popstate", () =>
|
|
1528
|
+
window.addEventListener("popstate", () => {
|
|
1529
|
+
syncTrackingContentCaptureForLocation();
|
|
1530
|
+
schedulePageview("popstate");
|
|
1531
|
+
});
|
|
1450
1532
|
}
|
|
1451
1533
|
|
|
1452
1534
|
function sendAgentNativeAnalytics(
|
|
@@ -192,6 +192,10 @@ export {
|
|
|
192
192
|
getChangeVersion,
|
|
193
193
|
bumpChangeVersion,
|
|
194
194
|
} from "./use-change-version.js";
|
|
195
|
+
export {
|
|
196
|
+
useDemoModeStatus,
|
|
197
|
+
type DemoModeStatus,
|
|
198
|
+
} from "./use-demo-mode-status.js";
|
|
195
199
|
export { useReconciledState } from "./use-external-value.js";
|
|
196
200
|
export {
|
|
197
201
|
buildDynamicAgentSuggestions,
|
|
@@ -751,6 +755,7 @@ export {
|
|
|
751
755
|
trackEvent,
|
|
752
756
|
trackSessionStatus,
|
|
753
757
|
configureTracking,
|
|
758
|
+
setTrackingContentCaptureEnabled,
|
|
754
759
|
maybeStartSessionReplay,
|
|
755
760
|
startSessionReplay,
|
|
756
761
|
stopSessionReplay,
|
|
@@ -109,6 +109,8 @@ export interface SessionReplayNetworkOptions {
|
|
|
109
109
|
|
|
110
110
|
export interface SessionReplayOptions {
|
|
111
111
|
enabled?: boolean;
|
|
112
|
+
/** Rechecked immediately before rrweb starts to cancel deferred startup. */
|
|
113
|
+
shouldStart?: () => boolean;
|
|
112
114
|
publicKey?: string;
|
|
113
115
|
endpoint?: string;
|
|
114
116
|
requireSignedInUser?: boolean;
|
|
@@ -208,6 +210,7 @@ interface NormalizedSessionReplayOptions {
|
|
|
208
210
|
/** Null disables network capture entirely. */
|
|
209
211
|
network: NormalizedCaptureOptions | null;
|
|
210
212
|
extraProperties?: SessionReplayOptions["extraProperties"];
|
|
213
|
+
shouldStart?: SessionReplayOptions["shouldStart"];
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
interface NormalizedCaptureOptions {
|
|
@@ -656,6 +659,7 @@ function normalizeOptions(
|
|
|
656
659
|
DEFAULT_MAX_NETWORK_EVENTS,
|
|
657
660
|
),
|
|
658
661
|
extraProperties: options.extraProperties,
|
|
662
|
+
shouldStart: options.shouldStart,
|
|
659
663
|
};
|
|
660
664
|
}
|
|
661
665
|
|
|
@@ -1950,6 +1954,9 @@ export async function startSessionReplay(
|
|
|
1950
1954
|
options: SessionReplayOptions = {},
|
|
1951
1955
|
): Promise<SessionReplayStartResult> {
|
|
1952
1956
|
if (options.enabled === false) return { started: false, reason: "disabled" };
|
|
1957
|
+
if (options.shouldStart && !options.shouldStart()) {
|
|
1958
|
+
return { started: false, reason: "disabled" };
|
|
1959
|
+
}
|
|
1953
1960
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1954
1961
|
return { started: false, reason: "not-browser" };
|
|
1955
1962
|
}
|
|
@@ -2032,6 +2039,9 @@ async function startSessionReplayRecorder(
|
|
|
2032
2039
|
} catch {
|
|
2033
2040
|
return { started: false, reason: "import-failed", sessionId, sampled };
|
|
2034
2041
|
}
|
|
2042
|
+
if (normalized.shouldStart && !normalized.shouldStart()) {
|
|
2043
|
+
return { started: false, reason: "disabled", sessionId, sampled };
|
|
2044
|
+
}
|
|
2035
2045
|
|
|
2036
2046
|
const replaySession = getOrCreateReplaySession(sessionId);
|
|
2037
2047
|
state.options = normalized;
|
|
@@ -13,40 +13,18 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { IconEyeOff } from "@tabler/icons-react";
|
|
16
|
-
import { useQuery } from "@tanstack/react-query";
|
|
17
16
|
import { useEffect, useState } from "react";
|
|
18
17
|
|
|
19
18
|
import { agentNativePath } from "../api-path.js";
|
|
20
|
-
import {
|
|
19
|
+
import { useDemoModeStatus } from "../use-demo-mode-status.js";
|
|
21
20
|
|
|
22
|
-
interface DemoStatus {
|
|
23
|
-
enabled?: boolean;
|
|
24
|
-
forced?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
|
|
28
21
|
const DEMO_MODE_WRITE_URL = agentNativePath(
|
|
29
22
|
"/_agent-native/application-state/demo-mode",
|
|
30
23
|
);
|
|
31
24
|
|
|
32
25
|
export function DemoModeSection() {
|
|
33
26
|
const [enabled, setEnabled] = useState<boolean | null>(null);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const { data } = useQuery({
|
|
37
|
-
queryKey: ["agent-native", "demo-mode", demoModeVersion],
|
|
38
|
-
queryFn: async () => {
|
|
39
|
-
const res = await fetch(DEMO_STATUS_URL, {
|
|
40
|
-
credentials: "same-origin",
|
|
41
|
-
});
|
|
42
|
-
if (!res.ok) return null;
|
|
43
|
-
return (await res.json()) as DemoStatus | null;
|
|
44
|
-
},
|
|
45
|
-
staleTime: Infinity,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const serverEnabled = data?.enabled;
|
|
49
|
-
const forced = data?.forced === true;
|
|
27
|
+
const { enabled: serverEnabled, forced, isLoading } = useDemoModeStatus();
|
|
50
28
|
|
|
51
29
|
// Surface the server value once it arrives (and on subsequent polls), but
|
|
52
30
|
// never clobber an in-flight optimistic toggle with a stale read. When
|
|
@@ -54,12 +32,10 @@ export function DemoModeSection() {
|
|
|
54
32
|
useEffect(() => {
|
|
55
33
|
if (forced) {
|
|
56
34
|
setEnabled(true);
|
|
57
|
-
} else if (
|
|
35
|
+
} else if (!isLoading) {
|
|
58
36
|
setEnabled((prev) => (prev === null ? serverEnabled : prev));
|
|
59
|
-
} else if (serverEnabled === undefined && data !== undefined) {
|
|
60
|
-
setEnabled((prev) => (prev === null ? false : prev));
|
|
61
37
|
}
|
|
62
|
-
}, [serverEnabled, forced,
|
|
38
|
+
}, [serverEnabled, forced, isLoading]);
|
|
63
39
|
|
|
64
40
|
const toggle = async (next: boolean) => {
|
|
65
41
|
if (forced) return;
|
|
@@ -12,6 +12,7 @@ const SENSITIVE_QUERY_PARAMS = new Set([
|
|
|
12
12
|
"code",
|
|
13
13
|
"share",
|
|
14
14
|
"share_token",
|
|
15
|
+
"bridge",
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
export function scrubUrl(url: string | undefined): string | undefined {
|
|
@@ -26,6 +27,18 @@ export function scrubUrl(url: string | undefined): string | undefined {
|
|
|
26
27
|
mutated = true;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
if (u.hash.includes("=")) {
|
|
31
|
+
const hashParams = new URLSearchParams(u.hash.slice(1));
|
|
32
|
+
let hashMutated = false;
|
|
33
|
+
for (const key of Array.from(hashParams.keys())) {
|
|
34
|
+
if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
|
|
35
|
+
hashParams.set(key, "<redacted>");
|
|
36
|
+
mutated = true;
|
|
37
|
+
hashMutated = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (hashMutated) u.hash = hashParams.toString();
|
|
41
|
+
}
|
|
29
42
|
if (!mutated) return url;
|
|
30
43
|
// If the original URL was relative, return only the path/query/fragment.
|
|
31
44
|
if (u.origin === "http://placeholder.local") {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import { agentNativePath } from "./api-path.js";
|
|
4
|
+
import { useChangeVersion } from "./use-change-version.js";
|
|
5
|
+
|
|
6
|
+
interface DemoModeStatusResponse {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
forced?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DemoModeStatus {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
forced: boolean;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads the effective Demo mode status for the current user. The shared
|
|
21
|
+
* change-version stream advances after Demo mode application state changes,
|
|
22
|
+
* so mounted consumers refresh without polling or duplicating the route call.
|
|
23
|
+
*/
|
|
24
|
+
export function useDemoModeStatus(): DemoModeStatus {
|
|
25
|
+
const demoModeVersion = useChangeVersion("app-state:demo-mode");
|
|
26
|
+
|
|
27
|
+
const { data, isLoading } = useQuery({
|
|
28
|
+
queryKey: ["agent-native", "demo-mode", demoModeVersion],
|
|
29
|
+
queryFn: async () => {
|
|
30
|
+
const res = await fetch(DEMO_STATUS_URL, {
|
|
31
|
+
credentials: "same-origin",
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
return (await res.json()) as DemoModeStatusResponse | null;
|
|
35
|
+
},
|
|
36
|
+
staleTime: Infinity,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
enabled: data?.enabled === true,
|
|
41
|
+
forced: data?.forced === true,
|
|
42
|
+
isLoading,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -39,6 +39,14 @@ details live in `.agents/skills/`.
|
|
|
39
39
|
Prometheus credential slot. Treat it as demo-environment data: do not use it
|
|
40
40
|
for `REAL_DATA_REQUIRED`, saved analyses, or real user analytics answers
|
|
41
41
|
unless the user explicitly asks to inspect the demo dashboard.
|
|
42
|
+
- Framework Demo mode is separate from the built-in `demo` source. While Demo
|
|
43
|
+
mode is enabled, Analytics line and area charts reshape each returned numeric
|
|
44
|
+
series into a deterministic, natural-looking upward trend using that series'
|
|
45
|
+
actual minimum, maximum, and normalized volatility. Smooth source series stay
|
|
46
|
+
smooth; spiky source series keep larger swings. This is presentation-only:
|
|
47
|
+
query results, tables, metrics, exports, and non-time-series charts remain
|
|
48
|
+
unchanged, so do not cite the displayed intermediate line/area points as
|
|
49
|
+
retrieved source values.
|
|
42
50
|
- Every analytical answer should include enough audit context for the user to
|
|
43
51
|
trust it: source(s), time window, filters, sample size or row count,
|
|
44
52
|
join/match method when relevant, and caveats/gaps.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useT } from "@agent-native/core/client";
|
|
1
|
+
import { useDemoModeStatus, useT } from "@agent-native/core/client";
|
|
2
2
|
import { EmbeddedExtension } from "@agent-native/core/client/extensions";
|
|
3
3
|
import {
|
|
4
4
|
IconArrowsSort,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
55
55
|
|
|
56
56
|
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
|
57
|
+
import { createDemoChartTrendRows } from "@/lib/demo-chart-trend";
|
|
57
58
|
import { useSqlQuery } from "@/lib/sql-query";
|
|
58
59
|
import { serializePanelSql } from "@/pages/adhoc/sql-dashboard/panel-sql";
|
|
59
60
|
import { pivotRows } from "@/pages/adhoc/sql-dashboard/pivot";
|
|
@@ -994,6 +995,7 @@ export function SqlChart({
|
|
|
994
995
|
onExportCsvChange,
|
|
995
996
|
}: SqlChartProps) {
|
|
996
997
|
const t = useT();
|
|
998
|
+
const { enabled: demoModeEnabled } = useDemoModeStatus();
|
|
997
999
|
// Hooks must be called unconditionally before any early return.
|
|
998
1000
|
const isSection = panel.chartType === "section";
|
|
999
1001
|
const isExtension = panel.chartType === "extension";
|
|
@@ -1024,7 +1026,7 @@ export function SqlChart({
|
|
|
1024
1026
|
const error =
|
|
1025
1027
|
rawRows.length === 0 ? (result?.error ?? queryErrorMessage) : undefined;
|
|
1026
1028
|
|
|
1027
|
-
const { rows, forcedYKeys } = useMemo(() => {
|
|
1029
|
+
const { rows: queryRows, forcedYKeys } = useMemo(() => {
|
|
1028
1030
|
if (panel.config?.pivot && rawRows.length) {
|
|
1029
1031
|
const pivoted = pivotRows(rawRows, panel.config.pivot);
|
|
1030
1032
|
return { rows: pivoted.rows, forcedYKeys: pivoted.seriesKeys };
|
|
@@ -1033,8 +1035,20 @@ export function SqlChart({
|
|
|
1033
1035
|
}, [rawRows, panel.config?.pivot]);
|
|
1034
1036
|
|
|
1035
1037
|
const { xKey, yKeys } = useMemo(
|
|
1036
|
-
() => detectKeys(
|
|
1037
|
-
[
|
|
1038
|
+
() => detectKeys(queryRows, panel.config, forcedYKeys),
|
|
1039
|
+
[queryRows, panel.config, forcedYKeys],
|
|
1040
|
+
);
|
|
1041
|
+
const shouldCreateDemoTrend =
|
|
1042
|
+
demoModeEnabled &&
|
|
1043
|
+
(panel.chartType === "line" ||
|
|
1044
|
+
panel.chartType === "area" ||
|
|
1045
|
+
(panel.chartType as string) === "stacked-area");
|
|
1046
|
+
const rows = useMemo(
|
|
1047
|
+
() =>
|
|
1048
|
+
shouldCreateDemoTrend
|
|
1049
|
+
? createDemoChartTrendRows(queryRows, yKeys, panel.id)
|
|
1050
|
+
: queryRows,
|
|
1051
|
+
[queryRows, yKeys, panel.id, shouldCreateDemoTrend],
|
|
1038
1052
|
);
|
|
1039
1053
|
|
|
1040
1054
|
// Section panels are pure layout — no query, no chart. Render a header with
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
type DemoTrendSeed = string | number;
|
|
2
|
+
|
|
3
|
+
function hashSeed(seed: string): number {
|
|
4
|
+
let hash = 2166136261;
|
|
5
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
6
|
+
hash ^= seed.charCodeAt(index);
|
|
7
|
+
hash = Math.imul(hash, 16777619);
|
|
8
|
+
}
|
|
9
|
+
return hash >>> 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createRandom(seed: string): () => number {
|
|
13
|
+
let state = hashSeed(seed);
|
|
14
|
+
return () => {
|
|
15
|
+
state += 0x6d2b79f5;
|
|
16
|
+
let value = state;
|
|
17
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
18
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
19
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
24
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function numericValue(value: unknown): number | null {
|
|
28
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
29
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
30
|
+
const numeric = Number(value);
|
|
31
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedVolatility(
|
|
35
|
+
series: number[],
|
|
36
|
+
minimum: number,
|
|
37
|
+
range: number,
|
|
38
|
+
) {
|
|
39
|
+
if (series.length < 3 || range === 0) return 0;
|
|
40
|
+
const normalized = series.map((value) => (value - minimum) / range);
|
|
41
|
+
const averageStep =
|
|
42
|
+
(normalized[normalized.length - 1] - normalized[0]) /
|
|
43
|
+
(normalized.length - 1);
|
|
44
|
+
const deviation = normalized
|
|
45
|
+
.slice(1)
|
|
46
|
+
.reduce(
|
|
47
|
+
(total, value, index) =>
|
|
48
|
+
total + Math.abs(value - normalized[index] - averageStep),
|
|
49
|
+
0,
|
|
50
|
+
);
|
|
51
|
+
return clamp((deviation / (normalized.length - 1)) * 1.4, 0, 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizedTrend(
|
|
55
|
+
length: number,
|
|
56
|
+
volatilityScore: number,
|
|
57
|
+
random: () => number,
|
|
58
|
+
): number[] {
|
|
59
|
+
if (length <= 1) return [0];
|
|
60
|
+
if (length === 2) return [0, 1];
|
|
61
|
+
|
|
62
|
+
const volatility = 0.015 + volatilityScore * (0.22 + random() * 0.12);
|
|
63
|
+
const noiseMemory = 0.78 - volatilityScore * 0.58;
|
|
64
|
+
let noise = 0;
|
|
65
|
+
const values = Array.from({ length }, (_, index) => {
|
|
66
|
+
if (index === 0) return 0;
|
|
67
|
+
if (index === length - 1) return 1;
|
|
68
|
+
|
|
69
|
+
const progress = index / (length - 1);
|
|
70
|
+
noise = noise * noiseMemory + (random() * 2 - 1) * (1 - noiseMemory);
|
|
71
|
+
const taperedNoise = noise * volatility * Math.sin(Math.PI * progress);
|
|
72
|
+
return clamp(progress + taperedNoise, 0.025, 0.975);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Pullback count and depth follow the source's normalized volatility. A
|
|
76
|
+
// smooth source remains a gentle rise; a jagged source gets several seeded
|
|
77
|
+
// reversals. Spacing the reversals across the series prevents them from
|
|
78
|
+
// collapsing into one noisy patch.
|
|
79
|
+
if (length >= 5 && volatilityScore >= 0.12) {
|
|
80
|
+
const maximumPullbacks = Math.min(3, Math.floor((length - 2) / 2));
|
|
81
|
+
const pullbackCount = Math.max(
|
|
82
|
+
1,
|
|
83
|
+
Math.round(volatilityScore * maximumPullbacks),
|
|
84
|
+
);
|
|
85
|
+
for (let pullback = 0; pullback < pullbackCount; pullback += 1) {
|
|
86
|
+
const segmentCenter =
|
|
87
|
+
((pullback + 1) * (length - 3)) / (pullbackCount + 1) + 1;
|
|
88
|
+
const jitter = (random() - 0.5) * Math.max(1, length / 10);
|
|
89
|
+
const dipIndex = Math.round(clamp(segmentCenter + jitter, 2, length - 2));
|
|
90
|
+
const center = dipIndex / (length - 1);
|
|
91
|
+
const amplitude = 0.025 + volatilityScore * (0.075 + random() * 0.12);
|
|
92
|
+
values[dipIndex - 1] = clamp(center + amplitude, 0.075, 0.925);
|
|
93
|
+
values[dipIndex] = clamp(center - amplitude, 0.05, 0.9);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return values;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Replace numeric chart series with a stable, seeded upward trend while
|
|
102
|
+
* retaining the query's original range and every non-series field. This is a
|
|
103
|
+
* presentation-only demo-mode transform: callers keep the real query result
|
|
104
|
+
* and opt individual line/area renderers into the returned rows.
|
|
105
|
+
*/
|
|
106
|
+
export function createDemoChartTrendRows(
|
|
107
|
+
rows: Record<string, unknown>[],
|
|
108
|
+
yKeys: string[],
|
|
109
|
+
seed: DemoTrendSeed,
|
|
110
|
+
): Record<string, unknown>[] {
|
|
111
|
+
if (rows.length === 0 || yKeys.length === 0) return rows;
|
|
112
|
+
|
|
113
|
+
let output: Record<string, unknown>[] | null = null;
|
|
114
|
+
|
|
115
|
+
for (const yKey of yKeys) {
|
|
116
|
+
const points = rows.flatMap((row, rowIndex) => {
|
|
117
|
+
const numeric = numericValue(row[yKey]);
|
|
118
|
+
return numeric === null
|
|
119
|
+
? []
|
|
120
|
+
: [{ rowIndex, numeric, original: row[yKey] }];
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (points.length < 2) continue;
|
|
124
|
+
|
|
125
|
+
const minimum = Math.min(...points.map((point) => point.numeric));
|
|
126
|
+
const maximum = Math.max(...points.map((point) => point.numeric));
|
|
127
|
+
if (minimum === maximum) continue;
|
|
128
|
+
|
|
129
|
+
const random = createRandom(`${String(seed)}:${yKey}`);
|
|
130
|
+
const range = maximum - minimum;
|
|
131
|
+
const volatilityScore = normalizedVolatility(
|
|
132
|
+
points.map((point) => point.numeric),
|
|
133
|
+
minimum,
|
|
134
|
+
range,
|
|
135
|
+
);
|
|
136
|
+
const trend = normalizedTrend(points.length, volatilityScore, random);
|
|
137
|
+
const useStringValues = points.every(
|
|
138
|
+
(point) => typeof point.original === "string",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
output ??= rows.map((row) => ({ ...row }));
|
|
142
|
+
points.forEach((point, pointIndex) => {
|
|
143
|
+
const numeric =
|
|
144
|
+
pointIndex === 0
|
|
145
|
+
? minimum
|
|
146
|
+
: pointIndex === points.length - 1
|
|
147
|
+
? maximum
|
|
148
|
+
: minimum + trend[pointIndex] * range;
|
|
149
|
+
output![point.rowIndex][yKey] = useStringValues
|
|
150
|
+
? String(numeric)
|
|
151
|
+
: numeric;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return output ?? rows;
|
|
156
|
+
}
|
|
@@ -56,6 +56,8 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
56
56
|
`[dashboard-report] Subscription ${sub.id} sent without a screenshot:`,
|
|
57
57
|
message,
|
|
58
58
|
);
|
|
59
|
+
await markDashboardReportResult(sub, "success", message);
|
|
60
|
+
continue;
|
|
59
61
|
}
|
|
60
62
|
await markDashboardReportResult(sub, "success");
|
|
61
63
|
} catch (err: any) {
|