@agent-native/core-corpus 0.216.0 → 0.218.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +3 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +12 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +28 -67
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The origin of a URL, or null when it has none. */
|
|
2
|
+
export function normalizeReplayOrigin(
|
|
3
|
+
value: string | null | undefined,
|
|
4
|
+
): string | null {
|
|
5
|
+
if (!value) return null;
|
|
6
|
+
try {
|
|
7
|
+
return new URL(value).origin;
|
|
8
|
+
// coercion-ok: a value that is not a URL names no origin, and callers treat null as "no origin".
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A replay key's allowed origins, stored as a JSON array or as comma- or newline-separated text. */
|
|
15
|
+
export function parseAllowedReplayOrigins(value: unknown): string[] {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value
|
|
18
|
+
.map((item) =>
|
|
19
|
+
normalizeReplayOrigin(typeof item === "string" ? item.trim() : null),
|
|
20
|
+
)
|
|
21
|
+
.filter((item): item is string => Boolean(item));
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "string" || !value.trim()) return [];
|
|
24
|
+
try {
|
|
25
|
+
return parseAllowedReplayOrigins(JSON.parse(value));
|
|
26
|
+
// coercion-ok: text that is not JSON is the comma- or newline-separated form.
|
|
27
|
+
} catch {
|
|
28
|
+
return value
|
|
29
|
+
.split(/[\n,]/)
|
|
30
|
+
.map((item) => normalizeReplayOrigin(item.trim()))
|
|
31
|
+
.filter((item): item is string => Boolean(item));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "drizzle-orm";
|
|
15
15
|
import { alias } from "drizzle-orm/pg-core";
|
|
16
16
|
|
|
17
|
+
import { OUTPUT_MADE_EVENT, outputUseRule } from "../../shared/output-use.js";
|
|
17
18
|
import {
|
|
18
19
|
AUTOMATIC_ANALYTICS_EVENT_NAMES,
|
|
19
20
|
type EventCatalogApp,
|
|
@@ -686,6 +687,18 @@ export async function listSessionEventNames(
|
|
|
686
687
|
};
|
|
687
688
|
}
|
|
688
689
|
|
|
690
|
+
function outputEventRole(
|
|
691
|
+
app: string | null,
|
|
692
|
+
eventName: string,
|
|
693
|
+
): EventCatalogEntry["outputRole"] {
|
|
694
|
+
const rule = outputUseRule(app);
|
|
695
|
+
if (!rule) return null;
|
|
696
|
+
if (rule.madeEvents.includes(eventName)) return "made";
|
|
697
|
+
return rule.usedEvents.some((event) => event.eventName === eventName)
|
|
698
|
+
? "used"
|
|
699
|
+
: null;
|
|
700
|
+
}
|
|
701
|
+
|
|
689
702
|
export function isAutomaticAnalyticsEvent(eventName: string): boolean {
|
|
690
703
|
return AUTOMATIC_ANALYTICS_EVENT_NAMES.has(eventName);
|
|
691
704
|
}
|
|
@@ -798,6 +811,7 @@ export async function listEventCatalog(
|
|
|
798
811
|
app: c.app,
|
|
799
812
|
eventCount: sql<number>`count(distinct ${c.eventName})`,
|
|
800
813
|
customEventCount: sql<number>`count(distinct ${c.eventName}) filter (where ${notInArray(c.eventName, [...AUTOMATIC_ANALYTICS_EVENT_NAMES])})`,
|
|
814
|
+
madeEventCount: sql<number>`count(*) filter (where ${eq(c.eventName, OUTPUT_MADE_EVENT)})`,
|
|
801
815
|
volume: sql<number>`sum(${c.eventCount})`,
|
|
802
816
|
})
|
|
803
817
|
.from(c)
|
|
@@ -837,6 +851,7 @@ export async function listEventCatalog(
|
|
|
837
851
|
description: null,
|
|
838
852
|
automatic: isAutomaticAnalyticsEvent(row.eventName),
|
|
839
853
|
stoppedFiring: false,
|
|
854
|
+
outputRole: outputEventRole(row.app, row.eventName),
|
|
840
855
|
};
|
|
841
856
|
},
|
|
842
857
|
);
|
|
@@ -861,12 +876,17 @@ export async function listEventCatalog(
|
|
|
861
876
|
app: string;
|
|
862
877
|
eventCount: unknown;
|
|
863
878
|
customEventCount: unknown;
|
|
879
|
+
madeEventCount: unknown;
|
|
864
880
|
volume: unknown;
|
|
865
881
|
}) => ({
|
|
866
882
|
app: row.app || null,
|
|
867
883
|
eventCount: Number(row.eventCount),
|
|
868
884
|
volume: Number(row.volume),
|
|
869
885
|
onlyAutomaticEvents: Number(row.customEventCount) === 0,
|
|
886
|
+
makesOutputs: Number(row.madeEventCount) > 0,
|
|
887
|
+
usedItEvents:
|
|
888
|
+
outputUseRule(row.app)?.usedEvents.map((event) => event.eventName) ??
|
|
889
|
+
[],
|
|
870
890
|
}),
|
|
871
891
|
);
|
|
872
892
|
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { fail } from "@agent-native/core/action";
|
|
2
|
-
import {
|
|
3
|
-
and,
|
|
4
|
-
asc,
|
|
5
|
-
desc,
|
|
6
|
-
eq,
|
|
7
|
-
gte,
|
|
8
|
-
inArray,
|
|
9
|
-
lt,
|
|
10
|
-
lte,
|
|
11
|
-
ne,
|
|
12
|
-
or,
|
|
13
|
-
sql,
|
|
14
|
-
} from "drizzle-orm";
|
|
2
|
+
import { and, desc, eq, gte, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
15
3
|
import { alias } from "drizzle-orm/pg-core";
|
|
16
4
|
|
|
17
5
|
import {
|
|
@@ -50,7 +38,10 @@ import {
|
|
|
50
38
|
viewerTenantKeys,
|
|
51
39
|
warnIndexFailure,
|
|
52
40
|
} from "./session-event-index.js";
|
|
53
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
listRecordingSessionAssociations,
|
|
43
|
+
sessionRecordingAssociationsReady,
|
|
44
|
+
} from "./session-recording-associations.js";
|
|
54
45
|
|
|
55
46
|
/**
|
|
56
47
|
* Performance aggregates.
|
|
@@ -69,7 +60,6 @@ const MAX_ROUTE_LENGTH = 200;
|
|
|
69
60
|
const MAX_APP_LENGTH = 100;
|
|
70
61
|
const MAX_SAMPLE_WEIGHT = 10_000;
|
|
71
62
|
const SESSION_PERFORMANCE_RETENTION_BUFFER_DAYS = 2;
|
|
72
|
-
const ASSOCIATION_READ_PAGE_SIZE = 500;
|
|
73
63
|
// Leave room for tenant and other filters under PostgreSQL's bind limit.
|
|
74
64
|
const SESSION_PERFORMANCE_SESSION_BATCH_SIZE = 25_000;
|
|
75
65
|
export const ROUTE_PERFORMANCE_RETENTION_DAYS = 180;
|
|
@@ -700,30 +690,6 @@ export async function getPerformanceCoverageStart(
|
|
|
700
690
|
return starts.at(-1) ?? null;
|
|
701
691
|
}
|
|
702
692
|
|
|
703
|
-
async function recordingSessionAssociations(
|
|
704
|
-
db: any,
|
|
705
|
-
recordingIds: readonly string[],
|
|
706
|
-
): Promise<Array<{ recordingId: string; sessionId: string }>> {
|
|
707
|
-
const association = schema.sessionRecordingSessionAssociations;
|
|
708
|
-
const result: Array<{ recordingId: string; sessionId: string }> = [];
|
|
709
|
-
let offset = 0;
|
|
710
|
-
while (true) {
|
|
711
|
-
const page = await db
|
|
712
|
-
.select({
|
|
713
|
-
recordingId: association.recordingId,
|
|
714
|
-
sessionId: association.sessionId,
|
|
715
|
-
})
|
|
716
|
-
.from(association)
|
|
717
|
-
.where(inArray(association.recordingId, [...recordingIds]))
|
|
718
|
-
.orderBy(asc(association.recordingId), asc(association.sessionId))
|
|
719
|
-
.limit(ASSOCIATION_READ_PAGE_SIZE)
|
|
720
|
-
.offset(offset);
|
|
721
|
-
result.push(...page);
|
|
722
|
-
if (page.length < ASSOCIATION_READ_PAGE_SIZE) return result;
|
|
723
|
-
offset += page.length;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
693
|
function mergePerformanceSummaries(
|
|
728
694
|
first: SessionPerformanceSummary,
|
|
729
695
|
second: SessionPerformanceSummary,
|
|
@@ -775,7 +741,7 @@ export async function getSessionPerformanceSummaries(
|
|
|
775
741
|
const db = getDb() as any;
|
|
776
742
|
if (!(await performanceTablesExist(db))) return summaries;
|
|
777
743
|
const associationRows = (await sessionRecordingAssociationsReady(db))
|
|
778
|
-
? await
|
|
744
|
+
? await listRecordingSessionAssociations(
|
|
779
745
|
db,
|
|
780
746
|
recordings.map((recording) => recording.id),
|
|
781
747
|
)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { sql } from "drizzle-orm";
|
|
1
|
+
import { asc, inArray, sql } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { schema } from "../db/index.js";
|
|
4
|
+
|
|
5
|
+
const ASSOCIATION_READ_PAGE_SIZE = 500;
|
|
2
6
|
|
|
3
7
|
const readyDatabases = new WeakSet<object>();
|
|
4
8
|
|
|
@@ -26,3 +30,28 @@ export async function sessionRecordingAssociationsReady(
|
|
|
26
30
|
}
|
|
27
31
|
throw new Error("Postgres table existence check returned an invalid value");
|
|
28
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Every session each recording observed, read in pages. */
|
|
35
|
+
export async function listRecordingSessionAssociations(
|
|
36
|
+
db: any,
|
|
37
|
+
recordingIds: readonly string[],
|
|
38
|
+
): Promise<Array<{ recordingId: string; sessionId: string }>> {
|
|
39
|
+
const association = schema.sessionRecordingSessionAssociations;
|
|
40
|
+
const result: Array<{ recordingId: string; sessionId: string }> = [];
|
|
41
|
+
let offset = 0;
|
|
42
|
+
while (true) {
|
|
43
|
+
const page = await db
|
|
44
|
+
.select({
|
|
45
|
+
recordingId: association.recordingId,
|
|
46
|
+
sessionId: association.sessionId,
|
|
47
|
+
})
|
|
48
|
+
.from(association)
|
|
49
|
+
.where(inArray(association.recordingId, [...recordingIds]))
|
|
50
|
+
.orderBy(asc(association.recordingId), asc(association.sessionId))
|
|
51
|
+
.limit(ASSOCIATION_READ_PAGE_SIZE)
|
|
52
|
+
.offset(offset);
|
|
53
|
+
result.push(...page);
|
|
54
|
+
if (page.length < ASSOCIATION_READ_PAGE_SIZE) return result;
|
|
55
|
+
offset += page.length;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getSessionRecordingOutputs } from "./output-outcomes.js";
|
|
2
|
+
import { listRecordingFriction } from "./session-friction.js";
|
|
3
|
+
import {
|
|
4
|
+
getSessionRecordingPerformance,
|
|
5
|
+
type SessionRecordingSummary,
|
|
6
|
+
type SessionReplayScope,
|
|
7
|
+
} from "./session-replay.js";
|
|
8
|
+
import {
|
|
9
|
+
isSessionsTriageLabEnabled,
|
|
10
|
+
sessionsTriageReadFailure,
|
|
11
|
+
} from "./sessions-triage-lab.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One replay's summary plus what the Sessions triage Lab adds to its page:
|
|
15
|
+
* friction, speed, and outputs, each replaced by its error when the read
|
|
16
|
+
* fails. The summary action and `view-screen` both return this, so the agent
|
|
17
|
+
* reads what the replay page shows.
|
|
18
|
+
*/
|
|
19
|
+
export async function withSessionsTriage(
|
|
20
|
+
summary: SessionRecordingSummary,
|
|
21
|
+
scope: SessionReplayScope,
|
|
22
|
+
logPrefix: string,
|
|
23
|
+
) {
|
|
24
|
+
let labEnabled: boolean;
|
|
25
|
+
try {
|
|
26
|
+
labEnabled = await isSessionsTriageLabEnabled(
|
|
27
|
+
scope.userEmail,
|
|
28
|
+
scope.orgId ?? null,
|
|
29
|
+
);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
...summary,
|
|
33
|
+
labStateError: sessionsTriageReadFailure("labState", logPrefix, error),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!labEnabled) return summary;
|
|
37
|
+
const [friction, speed, outputs] = await Promise.allSettled([
|
|
38
|
+
listRecordingFriction(scope, [summary.id]),
|
|
39
|
+
getSessionRecordingPerformance(scope, [summary.id]),
|
|
40
|
+
getSessionRecordingOutputs(scope, [summary.id]),
|
|
41
|
+
]);
|
|
42
|
+
const recordingFriction =
|
|
43
|
+
friction.status === "fulfilled" ? friction.value[summary.id] : undefined;
|
|
44
|
+
const recordingSpeed =
|
|
45
|
+
speed.status === "fulfilled"
|
|
46
|
+
? speed.value.performance[summary.id]
|
|
47
|
+
: undefined;
|
|
48
|
+
const recordingOutputs =
|
|
49
|
+
outputs.status === "fulfilled"
|
|
50
|
+
? outputs.value.outputs[summary.id]
|
|
51
|
+
: undefined;
|
|
52
|
+
const omittedOutputs =
|
|
53
|
+
outputs.status === "fulfilled"
|
|
54
|
+
? outputs.value.omittedOutputs[summary.id]
|
|
55
|
+
: undefined;
|
|
56
|
+
return {
|
|
57
|
+
...summary,
|
|
58
|
+
...(friction.status === "rejected"
|
|
59
|
+
? {
|
|
60
|
+
frictionError: sessionsTriageReadFailure(
|
|
61
|
+
"friction",
|
|
62
|
+
logPrefix,
|
|
63
|
+
friction.reason,
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
: recordingFriction
|
|
67
|
+
? { friction: recordingFriction }
|
|
68
|
+
: { frictionError: "Friction read skipped this recording" }),
|
|
69
|
+
...(speed.status === "rejected"
|
|
70
|
+
? {
|
|
71
|
+
performanceError: sessionsTriageReadFailure(
|
|
72
|
+
"speed",
|
|
73
|
+
logPrefix,
|
|
74
|
+
speed.reason,
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
: recordingSpeed !== undefined
|
|
78
|
+
? {
|
|
79
|
+
performance: recordingSpeed,
|
|
80
|
+
performanceCoverageStartedAt: speed.value.coverageStartedAt,
|
|
81
|
+
}
|
|
82
|
+
: { performanceError: "Speed read skipped this recording" }),
|
|
83
|
+
...(outputs.status === "rejected"
|
|
84
|
+
? {
|
|
85
|
+
outputsError: sessionsTriageReadFailure(
|
|
86
|
+
"outputs",
|
|
87
|
+
logPrefix,
|
|
88
|
+
outputs.reason,
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
: recordingOutputs !== undefined
|
|
92
|
+
? {
|
|
93
|
+
outputs: recordingOutputs,
|
|
94
|
+
...(omittedOutputs ? { omittedOutputs } : {}),
|
|
95
|
+
outputCoverageStartedAt: outputs.value.coverageStartedAt,
|
|
96
|
+
}
|
|
97
|
+
: { outputsError: "Outputs read skipped this recording" }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -42,6 +42,10 @@ import {
|
|
|
42
42
|
type AnyColumn,
|
|
43
43
|
} from "drizzle-orm";
|
|
44
44
|
|
|
45
|
+
import type {
|
|
46
|
+
OutputUseApp,
|
|
47
|
+
SessionRecordingOutputs,
|
|
48
|
+
} from "../../shared/output-use.js";
|
|
45
49
|
import { isScreenshotSize } from "../../shared/png.js";
|
|
46
50
|
import {
|
|
47
51
|
isSessionFrictionSort,
|
|
@@ -66,7 +70,16 @@ import {
|
|
|
66
70
|
} from "./first-party-analytics.js";
|
|
67
71
|
import { MAX_SESSION_ID_LENGTH } from "./indexed-text.js";
|
|
68
72
|
import type { JourneyRecording, RecordingViewport } from "./journey-tree.js";
|
|
73
|
+
import {
|
|
74
|
+
getSessionRecordingOutputs,
|
|
75
|
+
pruneOutputOutcomes,
|
|
76
|
+
unusedOutputConditions,
|
|
77
|
+
} from "./output-outcomes.js";
|
|
69
78
|
import { canonicalReplayLinkTimestamp } from "./replay-link-timestamp.js";
|
|
79
|
+
import {
|
|
80
|
+
normalizeReplayOrigin,
|
|
81
|
+
parseAllowedReplayOrigins,
|
|
82
|
+
} from "./replay-origins.js";
|
|
70
83
|
import { parseIngestBody } from "./request-errors.js";
|
|
71
84
|
import {
|
|
72
85
|
pruneSessionEventIndex,
|
|
@@ -161,6 +174,8 @@ export interface SessionReplayListFilters {
|
|
|
161
174
|
includePerformance?: boolean;
|
|
162
175
|
/** Measured sessions that showed every one of these friction signals. */
|
|
163
176
|
frictionSignals?: SessionFrictionSignal[];
|
|
177
|
+
/** Sessions that made an output of this app and never used it in time. */
|
|
178
|
+
unused?: OutputUseApp;
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
export interface SessionReplayEventReadOptions {
|
|
@@ -407,34 +422,6 @@ function replayInteger(value: unknown): number | null {
|
|
|
407
422
|
return null;
|
|
408
423
|
}
|
|
409
424
|
|
|
410
|
-
function normalizeReplayOrigin(
|
|
411
|
-
value: string | null | undefined,
|
|
412
|
-
): string | null {
|
|
413
|
-
if (!value) return null;
|
|
414
|
-
try {
|
|
415
|
-
return new URL(value).origin;
|
|
416
|
-
} catch {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function parseAllowedReplayOrigins(value: unknown): string[] {
|
|
422
|
-
if (Array.isArray(value)) {
|
|
423
|
-
return value
|
|
424
|
-
.map((item) => normalizeReplayOrigin(replayString(item)))
|
|
425
|
-
.filter((item): item is string => Boolean(item));
|
|
426
|
-
}
|
|
427
|
-
if (typeof value !== "string" || !value.trim()) return [];
|
|
428
|
-
try {
|
|
429
|
-
return parseAllowedReplayOrigins(JSON.parse(value));
|
|
430
|
-
} catch {
|
|
431
|
-
return value
|
|
432
|
-
.split(/[\n,]/)
|
|
433
|
-
.map((item) => normalizeReplayOrigin(item.trim()))
|
|
434
|
-
.filter((item): item is string => Boolean(item));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
425
|
function positiveReplayLimit(value: unknown, fallback: number): number {
|
|
439
426
|
const parsed = replayInteger(value);
|
|
440
427
|
return parsed && parsed > 0 ? parsed : fallback;
|
|
@@ -2488,6 +2475,13 @@ export interface SessionRecordingPage {
|
|
|
2488
2475
|
* performance aggregates began, or null when they have not.
|
|
2489
2476
|
*/
|
|
2490
2477
|
performanceCoverageStartedAt?: string | null;
|
|
2478
|
+
/**
|
|
2479
|
+
* With an unused-output filter: each row's outputs by recording id, and
|
|
2480
|
+
* when the viewer's output tracking began, or null before it has.
|
|
2481
|
+
*/
|
|
2482
|
+
outputs?: SessionRecordingOutputs["outputs"];
|
|
2483
|
+
omittedOutputs?: SessionRecordingOutputs["omittedOutputs"];
|
|
2484
|
+
outputCoverageStartedAt?: string | null;
|
|
2491
2485
|
}
|
|
2492
2486
|
|
|
2493
2487
|
const personalEmailDomains = [...FREE_EMAIL_PROVIDER_DOMAINS];
|
|
@@ -2700,6 +2694,7 @@ export async function listSessionRecordingsPage(
|
|
|
2700
2694
|
didNotEvents: filters.didNotEvents,
|
|
2701
2695
|
})),
|
|
2702
2696
|
...(await slowSessionConditions(scope, filters.slow)),
|
|
2697
|
+
...(await unusedOutputConditions(scope, filters.unused)),
|
|
2703
2698
|
...(await sessionFrictionFilterConditions(scope, filters.frictionSignals)),
|
|
2704
2699
|
);
|
|
2705
2700
|
const appConditions = [...conditions];
|
|
@@ -2778,18 +2773,32 @@ export async function listSessionRecordingsPage(
|
|
|
2778
2773
|
const recordings: SessionRecordingSummary[] = rows.map((row: any) =>
|
|
2779
2774
|
rowToSessionRecordingSummary(row),
|
|
2780
2775
|
);
|
|
2781
|
-
const performance =
|
|
2776
|
+
const [performance, outputs] = await Promise.all([
|
|
2782
2777
|
filters.slow || filters.includePerformance
|
|
2783
|
-
?
|
|
2778
|
+
? sessionRecordingPerformance(scope, recordings, {
|
|
2784
2779
|
summaries: filters.includePerformance === true,
|
|
2785
2780
|
})
|
|
2786
|
-
: null
|
|
2781
|
+
: null,
|
|
2782
|
+
filters.unused
|
|
2783
|
+
? getSessionRecordingOutputs(
|
|
2784
|
+
scope,
|
|
2785
|
+
recordings.map((recording) => recording.id),
|
|
2786
|
+
)
|
|
2787
|
+
: null,
|
|
2788
|
+
]);
|
|
2787
2789
|
return {
|
|
2788
2790
|
recordings,
|
|
2789
2791
|
total,
|
|
2790
2792
|
...(performance
|
|
2791
2793
|
? { performanceCoverageStartedAt: performance.coverageStartedAt }
|
|
2792
2794
|
: {}),
|
|
2795
|
+
...(outputs
|
|
2796
|
+
? {
|
|
2797
|
+
outputs: outputs.outputs,
|
|
2798
|
+
omittedOutputs: outputs.omittedOutputs,
|
|
2799
|
+
outputCoverageStartedAt: outputs.coverageStartedAt,
|
|
2800
|
+
}
|
|
2801
|
+
: {}),
|
|
2793
2802
|
appCounts: appRows
|
|
2794
2803
|
.filter((row: { app: string | null }) => row.app)
|
|
2795
2804
|
.map((row: { app: string; count: number }) => ({
|
|
@@ -3655,6 +3664,11 @@ export async function runSessionReplayRetentionSweep(
|
|
|
3655
3664
|
} catch (err) {
|
|
3656
3665
|
console.warn("[session-replay] Performance aggregate pruning failed:", err);
|
|
3657
3666
|
}
|
|
3667
|
+
try {
|
|
3668
|
+
await pruneOutputOutcomes(replayRetentionDays(), now);
|
|
3669
|
+
} catch (err) {
|
|
3670
|
+
console.warn("[session-replay] Output outcome pruning failed:", err);
|
|
3671
|
+
}
|
|
3658
3672
|
return {
|
|
3659
3673
|
finalized: finalized.finalized,
|
|
3660
3674
|
expired: expired.expired,
|
|
@@ -17,6 +17,7 @@ const READ_FAILURES = {
|
|
|
17
17
|
labState: "Couldn't read the Sessions triage Lab state.",
|
|
18
18
|
friction: "Couldn't read session friction.",
|
|
19
19
|
speed: "Couldn't read session speed data.",
|
|
20
|
+
outputs: "Couldn't read session outputs.",
|
|
20
21
|
} as const;
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -33,7 +34,11 @@ export function sessionsTriageReadFailure(
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** What a request asked the Lab for, named in its 403. */
|
|
36
|
-
export type SessionsTriageLabFeature =
|
|
37
|
+
export type SessionsTriageLabFeature =
|
|
38
|
+
| "events"
|
|
39
|
+
| "friction"
|
|
40
|
+
| "speed"
|
|
41
|
+
| "outputs";
|
|
37
42
|
|
|
38
43
|
function labFeatureSubject(
|
|
39
44
|
features: readonly SessionsTriageLabFeature[],
|
|
@@ -43,7 +48,8 @@ function labFeatureSubject(
|
|
|
43
48
|
nouns.length < 3
|
|
44
49
|
? nouns.join(" and ")
|
|
45
50
|
: `${nouns.slice(0, -1).join(", ")}, and ${nouns[nouns.length - 1]}`;
|
|
46
|
-
const plural =
|
|
51
|
+
const plural =
|
|
52
|
+
nouns.length > 1 || nouns[0] === "events" || nouns[0] === "outputs";
|
|
47
53
|
return `Session ${list} ${plural ? "are" : "is"}`;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -1847,6 +1847,52 @@ CREATE UNIQUE INDEX IF NOT EXISTS dashboard_views_default_per_dashboard_idx
|
|
|
1847
1847
|
name: "analytics-content-coverage-config",
|
|
1848
1848
|
sql: CONTENT_COVERAGE_CONFIG_MIGRATION_SQL,
|
|
1849
1849
|
},
|
|
1850
|
+
{
|
|
1851
|
+
version: 182,
|
|
1852
|
+
name: "analytics-output-outcomes",
|
|
1853
|
+
sql: {
|
|
1854
|
+
// The coverage table comes last: its existence is how ingest and
|
|
1855
|
+
// reads know the other two exist.
|
|
1856
|
+
postgres: `CREATE TABLE IF NOT EXISTS analytics_output_outcomes (
|
|
1857
|
+
id TEXT PRIMARY KEY,
|
|
1858
|
+
tenant_key TEXT NOT NULL,
|
|
1859
|
+
owner_email TEXT NOT NULL,
|
|
1860
|
+
org_id TEXT,
|
|
1861
|
+
app TEXT NOT NULL,
|
|
1862
|
+
output_type TEXT NOT NULL,
|
|
1863
|
+
output_id TEXT NOT NULL,
|
|
1864
|
+
made_at TEXT,
|
|
1865
|
+
use_by TEXT,
|
|
1866
|
+
made_session_id TEXT,
|
|
1867
|
+
first_used_at TEXT,
|
|
1868
|
+
used_event TEXT,
|
|
1869
|
+
last_seen_at TEXT NOT NULL
|
|
1870
|
+
);
|
|
1871
|
+
CREATE UNIQUE INDEX IF NOT EXISTS analytics_output_outcomes_key_idx
|
|
1872
|
+
ON analytics_output_outcomes (tenant_key, app, output_id);
|
|
1873
|
+
CREATE INDEX IF NOT EXISTS analytics_output_outcomes_made_session_idx
|
|
1874
|
+
ON analytics_output_outcomes (tenant_key, made_session_id);
|
|
1875
|
+
CREATE INDEX IF NOT EXISTS analytics_output_outcomes_last_seen_idx
|
|
1876
|
+
ON analytics_output_outcomes (last_seen_at);
|
|
1877
|
+
CREATE TABLE IF NOT EXISTS analytics_output_outcome_gaps (
|
|
1878
|
+
id TEXT PRIMARY KEY,
|
|
1879
|
+
tenant_key TEXT NOT NULL,
|
|
1880
|
+
owner_email TEXT NOT NULL,
|
|
1881
|
+
org_id TEXT,
|
|
1882
|
+
first_at TEXT NOT NULL,
|
|
1883
|
+
last_at TEXT NOT NULL,
|
|
1884
|
+
recorded_at TEXT NOT NULL
|
|
1885
|
+
);
|
|
1886
|
+
CREATE INDEX IF NOT EXISTS analytics_output_outcome_gaps_tenant_idx
|
|
1887
|
+
ON analytics_output_outcome_gaps (tenant_key, last_at);
|
|
1888
|
+
CREATE TABLE IF NOT EXISTS analytics_output_outcome_coverage (
|
|
1889
|
+
tenant_key TEXT PRIMARY KEY,
|
|
1890
|
+
owner_email TEXT NOT NULL,
|
|
1891
|
+
org_id TEXT,
|
|
1892
|
+
started_at TEXT NOT NULL
|
|
1893
|
+
)`,
|
|
1894
|
+
},
|
|
1895
|
+
},
|
|
1850
1896
|
],
|
|
1851
1897
|
{ table: "analytics_migrations" },
|
|
1852
1898
|
);
|