@agent-native/core-corpus 0.216.0 → 0.217.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 +1 -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/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-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/calendar/CHANGELOG.md +6 -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/changelog/2026-10-21-solo-events-avoid-notification-approval.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 +8 -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/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/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-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-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/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 +2 -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/Index.tsx +10 -3
- 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-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/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/CHANGELOG.md +6 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
3
|
+
import { useLab } from "@agent-native/core/client/labs";
|
|
4
|
+
import {
|
|
5
|
+
IconExternalLink,
|
|
6
|
+
IconPackage,
|
|
7
|
+
IconRefresh,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import {
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverTrigger,
|
|
15
|
+
} from "@/components/ui/popover";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
import { ANALYTICS_SESSIONS_TRIAGE_LAB } from "../../../shared/labs";
|
|
19
|
+
import {
|
|
20
|
+
MIN_PRESENTATION_MS,
|
|
21
|
+
OUTPUT_USE_WINDOW_DAYS,
|
|
22
|
+
type OutputUseApp,
|
|
23
|
+
type SessionOutput,
|
|
24
|
+
type SessionRecordingOutputs,
|
|
25
|
+
} from "../../../shared/output-use";
|
|
26
|
+
|
|
27
|
+
type T = ReturnType<typeof useT>;
|
|
28
|
+
|
|
29
|
+
export function unusedFilterLabel(app: OutputUseApp, t: T): string {
|
|
30
|
+
switch (app) {
|
|
31
|
+
case "slides":
|
|
32
|
+
return t("sessions.unusedSlidesDecks");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function unusedRuleText(app: OutputUseApp, t: T): string {
|
|
37
|
+
switch (app) {
|
|
38
|
+
case "slides":
|
|
39
|
+
return t("sessions.unusedSlidesRule", {
|
|
40
|
+
days: String(OUTPUT_USE_WINDOW_DAYS),
|
|
41
|
+
seconds: String(MIN_PRESENTATION_MS / 1000),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function openOutputLabel(output: SessionOutput, t: T): string {
|
|
47
|
+
switch (output.app) {
|
|
48
|
+
case "slides":
|
|
49
|
+
return t("sessions.openDeck");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function outputTitle(output: SessionOutput, t: T): string {
|
|
54
|
+
const date = new Date(output.madeAt).toLocaleDateString();
|
|
55
|
+
switch (output.app) {
|
|
56
|
+
case "slides":
|
|
57
|
+
return t("sessions.deckMadeOn", { date });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function outputStatusText(output: SessionOutput, t: T): string {
|
|
62
|
+
switch (output.status) {
|
|
63
|
+
case "used": {
|
|
64
|
+
const date = output.usedAt
|
|
65
|
+
? new Date(output.usedAt).toLocaleDateString()
|
|
66
|
+
: "";
|
|
67
|
+
if (output.usedEvent === "presentation_ended") {
|
|
68
|
+
return t("sessions.outputPresentedOn", { date });
|
|
69
|
+
}
|
|
70
|
+
if (output.usedEvent === "deck_exported") {
|
|
71
|
+
return t("sessions.outputExportedOn", { date });
|
|
72
|
+
}
|
|
73
|
+
return t("sessions.outputUsedOn", { date });
|
|
74
|
+
}
|
|
75
|
+
case "unused":
|
|
76
|
+
return t("sessions.outputUnused", {
|
|
77
|
+
days: String(OUTPUT_USE_WINDOW_DAYS),
|
|
78
|
+
});
|
|
79
|
+
case "open":
|
|
80
|
+
return t("sessions.outputOpen", {
|
|
81
|
+
date: new Date(output.useBy).toLocaleDateString(),
|
|
82
|
+
});
|
|
83
|
+
case "unmeasured":
|
|
84
|
+
return t("sessions.outputUnmeasured");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function OutputLink({ output }: { output: SessionOutput }) {
|
|
89
|
+
const t = useT();
|
|
90
|
+
if (!output.url) return null;
|
|
91
|
+
return (
|
|
92
|
+
<Button asChild variant="ghost" size="xs" className="shrink-0">
|
|
93
|
+
<a href={output.url} target="_blank" rel="noreferrer">
|
|
94
|
+
{openOutputLabel(output, t)}
|
|
95
|
+
<IconExternalLink />
|
|
96
|
+
</a>
|
|
97
|
+
</Button>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A link cannot nest inside the row's replay link, so this sits beside it.
|
|
102
|
+
export function UnusedOutputLink({
|
|
103
|
+
outputs,
|
|
104
|
+
}: {
|
|
105
|
+
outputs?: SessionOutput[] | null;
|
|
106
|
+
}) {
|
|
107
|
+
const t = useT();
|
|
108
|
+
const unusedOutputs = (outputs ?? []).filter(
|
|
109
|
+
(output) => output.status === "unused",
|
|
110
|
+
);
|
|
111
|
+
const linked = unusedOutputs.find((output) => output.url);
|
|
112
|
+
if (!linked) return null;
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex shrink-0 items-center gap-1 px-3">
|
|
115
|
+
<OutputLink output={linked} />
|
|
116
|
+
{unusedOutputs.length > 1 ? (
|
|
117
|
+
<span className="text-xs text-muted-foreground">
|
|
118
|
+
{t("sessions.moreOutputs", {
|
|
119
|
+
count: String(unusedOutputs.length - 1),
|
|
120
|
+
})}
|
|
121
|
+
</span>
|
|
122
|
+
) : null}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The outputs a replay's session made, and whether each was used in time. */
|
|
128
|
+
export function SessionOutputsButton({ recordingId }: { recordingId: string }) {
|
|
129
|
+
const t = useT();
|
|
130
|
+
const lab = useLab(ANALYTICS_SESSIONS_TRIAGE_LAB);
|
|
131
|
+
const query = useActionQuery<SessionRecordingOutputs>(
|
|
132
|
+
"list-session-outputs",
|
|
133
|
+
{ recordingIds: [recordingId] },
|
|
134
|
+
{ enabled: lab, staleTime: 30_000 },
|
|
135
|
+
);
|
|
136
|
+
if (!lab) return null;
|
|
137
|
+
if (query.isError && !query.data) {
|
|
138
|
+
return (
|
|
139
|
+
<Button
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="sm"
|
|
142
|
+
onClick={() => void query.refetch()}
|
|
143
|
+
disabled={query.isFetching}
|
|
144
|
+
>
|
|
145
|
+
<IconRefresh className={cn(query.isFetching && "animate-spin")} />
|
|
146
|
+
{t("sessions.outputsUnavailable")}
|
|
147
|
+
</Button>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const outputs = query.data?.outputs[recordingId];
|
|
151
|
+
if (!outputs?.length) return null;
|
|
152
|
+
const omitted = query.data?.omittedOutputs[recordingId] ?? 0;
|
|
153
|
+
const total = outputs.length + omitted;
|
|
154
|
+
return (
|
|
155
|
+
<Popover>
|
|
156
|
+
<PopoverTrigger asChild>
|
|
157
|
+
<Button variant="outline">
|
|
158
|
+
<IconPackage />
|
|
159
|
+
{t(
|
|
160
|
+
total === 1 ? "sessions.deckCountSingular" : "sessions.deckCount",
|
|
161
|
+
{ count: String(total) },
|
|
162
|
+
)}
|
|
163
|
+
</Button>
|
|
164
|
+
</PopoverTrigger>
|
|
165
|
+
<PopoverContent align="end" className="w-80">
|
|
166
|
+
<ul className="space-y-3">
|
|
167
|
+
{outputs.map((output) => (
|
|
168
|
+
<li
|
|
169
|
+
key={`${output.app}:${output.outputId}`}
|
|
170
|
+
className="flex items-start justify-between gap-3"
|
|
171
|
+
>
|
|
172
|
+
<span className="min-w-0 text-sm">
|
|
173
|
+
<span className="block truncate font-medium">
|
|
174
|
+
{outputTitle(output, t)}
|
|
175
|
+
</span>
|
|
176
|
+
<span
|
|
177
|
+
className={cn(
|
|
178
|
+
"block text-xs text-muted-foreground",
|
|
179
|
+
output.status === "unused" && "text-foreground",
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{outputStatusText(output, t)}
|
|
183
|
+
</span>
|
|
184
|
+
</span>
|
|
185
|
+
<OutputLink output={output} />
|
|
186
|
+
</li>
|
|
187
|
+
))}
|
|
188
|
+
</ul>
|
|
189
|
+
{omitted ? (
|
|
190
|
+
<p className="mt-3 text-xs text-muted-foreground">
|
|
191
|
+
{t("sessions.moreOutputs", { count: String(omitted) })}
|
|
192
|
+
</p>
|
|
193
|
+
) : null}
|
|
194
|
+
</PopoverContent>
|
|
195
|
+
</Popover>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IconChevronLeft,
|
|
7
7
|
IconChevronRight,
|
|
8
8
|
IconGauge,
|
|
9
|
+
IconPackageOff,
|
|
9
10
|
IconPhoto,
|
|
10
11
|
IconRefresh,
|
|
11
12
|
IconX,
|
|
@@ -44,6 +45,14 @@ import { useReplayStorageStatus } from "@/hooks/use-replay-storage-status";
|
|
|
44
45
|
import { cn } from "@/lib/utils";
|
|
45
46
|
|
|
46
47
|
import { ANALYTICS_SESSIONS_TRIAGE_LAB } from "../../../shared/labs";
|
|
48
|
+
import {
|
|
49
|
+
isOutputUseApp,
|
|
50
|
+
OUTPUT_USE_APPS,
|
|
51
|
+
OUTPUT_USE_WINDOW_DAYS,
|
|
52
|
+
type OutputUseApp,
|
|
53
|
+
type SessionOutput,
|
|
54
|
+
UNUSED_OUTPUT_PARAM,
|
|
55
|
+
} from "../../../shared/output-use";
|
|
47
56
|
import {
|
|
48
57
|
sessionDateBound,
|
|
49
58
|
sessionDateForDisplay,
|
|
@@ -86,6 +95,11 @@ import {
|
|
|
86
95
|
SessionFrictionFilter,
|
|
87
96
|
SessionFrictionStrip,
|
|
88
97
|
} from "./SessionFriction";
|
|
98
|
+
import {
|
|
99
|
+
UnusedOutputLink,
|
|
100
|
+
unusedFilterLabel,
|
|
101
|
+
unusedRuleText,
|
|
102
|
+
} from "./SessionOutputs";
|
|
89
103
|
import { SessionReplayStoryboardExportDialog } from "./SessionReplayStoryboardExportDialog";
|
|
90
104
|
import {
|
|
91
105
|
EmptySessionsState,
|
|
@@ -128,6 +142,10 @@ type Page = {
|
|
|
128
142
|
frictionCoverageStartedAt?: string | null;
|
|
129
143
|
/** Present when a speed filter applied; null: nothing measured yet. */
|
|
130
144
|
performanceCoverageStartedAt?: string | null;
|
|
145
|
+
/** Present when the unused filter applied, by recording id. */
|
|
146
|
+
outputs?: Record<string, SessionOutput[] | null>;
|
|
147
|
+
/** Present when the unused filter applied; null: nothing tracked yet. */
|
|
148
|
+
outputCoverageStartedAt?: string | null;
|
|
131
149
|
};
|
|
132
150
|
|
|
133
151
|
const RANGES: Range[] = ["24h", "7d", "30d", "90d", "all"];
|
|
@@ -303,11 +321,18 @@ export function SessionsTriagePage() {
|
|
|
303
321
|
const slow: SlowSessionFilter | undefined =
|
|
304
322
|
eventsLabEnabled && isSlowSessionFilter(urlSlow) ? urlSlow : undefined;
|
|
305
323
|
const urlHasSlowFilter = isSlowSessionFilter(urlSlow);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
324
|
+
const urlUnused = params.get(UNUSED_OUTPUT_PARAM);
|
|
325
|
+
const unused: OutputUseApp | undefined =
|
|
326
|
+
eventsLabEnabled && isOutputUseApp(urlUnused) ? urlUnused : undefined;
|
|
327
|
+
const urlHasUnusedFilter = isOutputUseApp(urlUnused);
|
|
328
|
+
// A shared link with event, friction, slow, or unused conditions waits
|
|
329
|
+
// briefly for the Lab state instead of listing unfiltered sessions. Nothing
|
|
330
|
+
// else waits, and a hung read stops holding the list and reads as failed.
|
|
309
331
|
const urlHasLabFilters =
|
|
310
|
-
urlHasEventConditions ||
|
|
332
|
+
urlHasEventConditions ||
|
|
333
|
+
urlHasFrictionParams ||
|
|
334
|
+
urlHasSlowFilter ||
|
|
335
|
+
urlHasUnusedFilter;
|
|
311
336
|
const [labWaitExpired, setLabWaitExpired] = useState(false);
|
|
312
337
|
const waitsForLab = urlHasLabFilters && eventsLab.isLoading;
|
|
313
338
|
useEffect(() => {
|
|
@@ -445,6 +470,7 @@ export function SessionsTriagePage() {
|
|
|
445
470
|
frictionSignals: frictionSignals.length ? frictionSignals : undefined,
|
|
446
471
|
includeFriction: frictionApplied || undefined,
|
|
447
472
|
slow,
|
|
473
|
+
unused,
|
|
448
474
|
sort,
|
|
449
475
|
offset: (page - 1) * SESSION_PAGE_SIZE,
|
|
450
476
|
limit: SESSION_PAGE_SIZE,
|
|
@@ -491,6 +517,18 @@ export function SessionsTriagePage() {
|
|
|
491
517
|
: t("sessions.speedCoverageSince", {
|
|
492
518
|
date: new Date(speedCoverageStartedAt).toLocaleDateString(),
|
|
493
519
|
});
|
|
520
|
+
const outputCoverageStartedAt = data?.outputCoverageStartedAt;
|
|
521
|
+
const outputCoverageNote =
|
|
522
|
+
outputCoverageStartedAt === undefined
|
|
523
|
+
? null
|
|
524
|
+
: outputCoverageStartedAt === null
|
|
525
|
+
? t("sessions.outputCoverageStarting", {
|
|
526
|
+
days: String(OUTPUT_USE_WINDOW_DAYS),
|
|
527
|
+
})
|
|
528
|
+
: t("sessions.outputCoverageSince", {
|
|
529
|
+
date: new Date(outputCoverageStartedAt).toLocaleDateString(),
|
|
530
|
+
days: String(OUTPUT_USE_WINDOW_DAYS),
|
|
531
|
+
});
|
|
494
532
|
const total = data?.total ?? 0;
|
|
495
533
|
// Speed hints load beside the list, keyed on its rows, so turning the Lab
|
|
496
534
|
// on adds them without fetching the list again.
|
|
@@ -539,6 +577,7 @@ export function SessionsTriagePage() {
|
|
|
539
577
|
: undefined,
|
|
540
578
|
frictionSignals: frictionSignals.length ? frictionSignals : undefined,
|
|
541
579
|
slow,
|
|
580
|
+
unused,
|
|
542
581
|
sort,
|
|
543
582
|
limit: 1,
|
|
544
583
|
},
|
|
@@ -909,6 +948,54 @@ export function SessionsTriagePage() {
|
|
|
909
948
|
</PopoverContent>
|
|
910
949
|
</Popover>
|
|
911
950
|
) : null}
|
|
951
|
+
{eventsLabEnabled ? (
|
|
952
|
+
<Popover>
|
|
953
|
+
<PopoverTrigger asChild>
|
|
954
|
+
<Button
|
|
955
|
+
variant={unused ? "secondary" : "outline"}
|
|
956
|
+
size="sm"
|
|
957
|
+
className={cn(
|
|
958
|
+
"h-8 border border-input font-normal",
|
|
959
|
+
!unused && "bg-transparent hover:bg-accent",
|
|
960
|
+
)}
|
|
961
|
+
>
|
|
962
|
+
<IconPackageOff />
|
|
963
|
+
{unused
|
|
964
|
+
? unusedFilterLabel(unused, t)
|
|
965
|
+
: t("sessions.unusedOutputs")}
|
|
966
|
+
</Button>
|
|
967
|
+
</PopoverTrigger>
|
|
968
|
+
<PopoverContent align="start" className="w-72">
|
|
969
|
+
<div className="space-y-3">
|
|
970
|
+
<Select
|
|
971
|
+
value={unused ?? "all"}
|
|
972
|
+
onValueChange={(value) =>
|
|
973
|
+
setFilter(UNUSED_OUTPUT_PARAM, value === "all" ? "" : value)
|
|
974
|
+
}
|
|
975
|
+
>
|
|
976
|
+
<SelectTrigger aria-label={t("sessions.unusedOutputs")}>
|
|
977
|
+
<SelectValue />
|
|
978
|
+
</SelectTrigger>
|
|
979
|
+
<SelectContent>
|
|
980
|
+
<SelectItem value="all">
|
|
981
|
+
{t("sessions.anyOutcome")}
|
|
982
|
+
</SelectItem>
|
|
983
|
+
{OUTPUT_USE_APPS.map((value) => (
|
|
984
|
+
<SelectItem key={value} value={value}>
|
|
985
|
+
{unusedFilterLabel(value, t)}
|
|
986
|
+
</SelectItem>
|
|
987
|
+
))}
|
|
988
|
+
</SelectContent>
|
|
989
|
+
</Select>
|
|
990
|
+
{OUTPUT_USE_APPS.map((value) => (
|
|
991
|
+
<p key={value} className="text-xs text-muted-foreground">
|
|
992
|
+
{unusedRuleText(value, t)}
|
|
993
|
+
</p>
|
|
994
|
+
))}
|
|
995
|
+
</div>
|
|
996
|
+
</PopoverContent>
|
|
997
|
+
</Popover>
|
|
998
|
+
) : null}
|
|
912
999
|
{hasActiveFilters ? (
|
|
913
1000
|
<Button
|
|
914
1001
|
variant="ghost"
|
|
@@ -959,6 +1046,14 @@ export function SessionsTriagePage() {
|
|
|
959
1046
|
{t("sessions.speedFilterNeedsLab")}
|
|
960
1047
|
</p>
|
|
961
1048
|
) : null}
|
|
1049
|
+
{urlHasUnusedFilter &&
|
|
1050
|
+
!eventsLabEnabled &&
|
|
1051
|
+
!eventsLab.isLoading &&
|
|
1052
|
+
!eventsLab.isError ? (
|
|
1053
|
+
<p className="text-xs text-muted-foreground" role="status">
|
|
1054
|
+
{t("sessions.unusedFilterNeedsLab")}
|
|
1055
|
+
</p>
|
|
1056
|
+
) : null}
|
|
962
1057
|
{eventsLabEnabled &&
|
|
963
1058
|
!frictionApplied &&
|
|
964
1059
|
rowFrictionError &&
|
|
@@ -1012,6 +1107,13 @@ export function SessionsTriagePage() {
|
|
|
1012
1107
|
{speedCoverageNote}
|
|
1013
1108
|
</p>
|
|
1014
1109
|
) : null}
|
|
1110
|
+
{outputCoverageStartedAt !== undefined &&
|
|
1111
|
+
recordings.length > 0 &&
|
|
1112
|
+
rangePredatesCoverage(dateBounds.from, outputCoverageStartedAt) ? (
|
|
1113
|
+
<p className="text-xs text-muted-foreground" role="status">
|
|
1114
|
+
{outputCoverageNote}
|
|
1115
|
+
</p>
|
|
1116
|
+
) : null}
|
|
1015
1117
|
<Card>
|
|
1016
1118
|
<div className="flex items-center justify-between gap-2 border-b px-4 py-2 text-sm">
|
|
1017
1119
|
<div className="text-muted-foreground" aria-live="polite">
|
|
@@ -1099,6 +1201,9 @@ export function SessionsTriagePage() {
|
|
|
1099
1201
|
{speedCoverageNote ? (
|
|
1100
1202
|
<p className="mt-1 text-xs">{speedCoverageNote}</p>
|
|
1101
1203
|
) : null}
|
|
1204
|
+
{outputCoverageNote ? (
|
|
1205
|
+
<p className="mt-1 text-xs">{outputCoverageNote}</p>
|
|
1206
|
+
) : null}
|
|
1102
1207
|
{showEmptySessionRecovery ? (
|
|
1103
1208
|
<Button
|
|
1104
1209
|
variant="outline"
|
|
@@ -1273,6 +1378,11 @@ export function SessionsTriagePage() {
|
|
|
1273
1378
|
/>
|
|
1274
1379
|
</span>
|
|
1275
1380
|
</Link>
|
|
1381
|
+
{unused ? (
|
|
1382
|
+
<UnusedOutputLink
|
|
1383
|
+
outputs={data?.outputs?.[recording.id]}
|
|
1384
|
+
/>
|
|
1385
|
+
) : null}
|
|
1276
1386
|
</div>
|
|
1277
1387
|
</SessionRow>
|
|
1278
1388
|
))}
|
|
@@ -654,6 +654,73 @@ export const analyticsPerformanceCoverage = table(
|
|
|
654
654
|
},
|
|
655
655
|
);
|
|
656
656
|
|
|
657
|
+
// Each output an app reported making, from `shared/output-use.ts`, and its
|
|
658
|
+
// first use. A null madeAt means only uses have arrived so far; useBy is
|
|
659
|
+
// madeAt plus the use window, so "used in time" compares two stored values.
|
|
660
|
+
export const analyticsOutputOutcomes = table(
|
|
661
|
+
"analytics_output_outcomes",
|
|
662
|
+
{
|
|
663
|
+
id: text("id").primaryKey(),
|
|
664
|
+
tenantKey: text("tenant_key").notNull(),
|
|
665
|
+
ownerEmail: text("owner_email").notNull(),
|
|
666
|
+
orgId: text("org_id"),
|
|
667
|
+
app: text("app").notNull(),
|
|
668
|
+
outputType: text("output_type").notNull(),
|
|
669
|
+
outputId: text("output_id").notNull(),
|
|
670
|
+
madeAt: text("made_at"),
|
|
671
|
+
useBy: text("use_by"),
|
|
672
|
+
madeSessionId: text("made_session_id"),
|
|
673
|
+
firstUsedAt: text("first_used_at"),
|
|
674
|
+
usedEvent: text("used_event"),
|
|
675
|
+
lastSeenAt: text("last_seen_at").notNull(),
|
|
676
|
+
},
|
|
677
|
+
(t) => ({
|
|
678
|
+
outputOutcomeUnique: uniqueIndex("analytics_output_outcomes_key_idx").on(
|
|
679
|
+
t.tenantKey,
|
|
680
|
+
t.app,
|
|
681
|
+
t.outputId,
|
|
682
|
+
),
|
|
683
|
+
madeSessionIdx: index("analytics_output_outcomes_made_session_idx").on(
|
|
684
|
+
t.tenantKey,
|
|
685
|
+
t.madeSessionId,
|
|
686
|
+
),
|
|
687
|
+
lastSeenIdx: index("analytics_output_outcomes_last_seen_idx").on(
|
|
688
|
+
t.lastSeenAt,
|
|
689
|
+
),
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// Event-time spans in which a failed write may have dropped an output's use,
|
|
694
|
+
// so no output made in or before one can read as unused.
|
|
695
|
+
export const analyticsOutputOutcomeGaps = table(
|
|
696
|
+
"analytics_output_outcome_gaps",
|
|
697
|
+
{
|
|
698
|
+
id: text("id").primaryKey(),
|
|
699
|
+
tenantKey: text("tenant_key").notNull(),
|
|
700
|
+
ownerEmail: text("owner_email").notNull(),
|
|
701
|
+
orgId: text("org_id"),
|
|
702
|
+
firstAt: text("first_at").notNull(),
|
|
703
|
+
lastAt: text("last_at").notNull(),
|
|
704
|
+
recordedAt: text("recorded_at").notNull(),
|
|
705
|
+
},
|
|
706
|
+
(t) => ({
|
|
707
|
+
outputOutcomeGapTenantIdx: index(
|
|
708
|
+
"analytics_output_outcome_gaps_tenant_idx",
|
|
709
|
+
).on(t.tenantKey, t.lastAt),
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// When each tenant's output outcomes began.
|
|
714
|
+
export const analyticsOutputOutcomeCoverage = table(
|
|
715
|
+
"analytics_output_outcome_coverage",
|
|
716
|
+
{
|
|
717
|
+
tenantKey: text("tenant_key").primaryKey(),
|
|
718
|
+
ownerEmail: text("owner_email").notNull(),
|
|
719
|
+
orgId: text("org_id"),
|
|
720
|
+
startedAt: text("started_at").notNull(),
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
|
|
657
724
|
export const analyticsUserDays = table("analytics_user_days", {
|
|
658
725
|
id: text("id").primaryKey(),
|
|
659
726
|
tenantKey: text("tenant_key").notNull(),
|
|
@@ -651,6 +651,15 @@ const ORIGIN_MAIN_PANEL_REPLACEMENTS: readonly FingerprintedPanelReplacement[] =
|
|
|
651
651
|
"ba2e861c2044e2fec669bcb15a878a675eaf426f51bcfe4db32ebd228d2050f0",
|
|
652
652
|
sql: buildPanel("activation-funnel")!.sql,
|
|
653
653
|
},
|
|
654
|
+
// The funnel before it joined visitors without analytics consent to
|
|
655
|
+
// their signup through the session.
|
|
656
|
+
{
|
|
657
|
+
id: "activation-funnel",
|
|
658
|
+
source: "first-party",
|
|
659
|
+
sha256:
|
|
660
|
+
"21df6cc1f26862531504c21fb5a558c87b58615118fcd0ad1913a971fa8387a6",
|
|
661
|
+
sql: buildPanel("activation-funnel")!.sql,
|
|
662
|
+
},
|
|
654
663
|
{
|
|
655
664
|
id: "retention-over-time",
|
|
656
665
|
source: "bigquery",
|
|
@@ -74,6 +74,10 @@ import {
|
|
|
74
74
|
boundedIdentity,
|
|
75
75
|
boundedText,
|
|
76
76
|
} from "./indexed-text.js";
|
|
77
|
+
import {
|
|
78
|
+
recordLostOutputBatch,
|
|
79
|
+
recordOutputOutcomes,
|
|
80
|
+
} from "./output-outcomes.js";
|
|
77
81
|
import { parseIngestBody, requestError } from "./request-errors.js";
|
|
78
82
|
import {
|
|
79
83
|
recordEventCatalog,
|
|
@@ -243,6 +247,7 @@ async function persistBigQueryRowsWithMigrationFallback(
|
|
|
243
247
|
);
|
|
244
248
|
await recordSessionEventIndex(tx, rows, receivedAt);
|
|
245
249
|
await recordSessionPerformance(tx, rows, receivedAt);
|
|
250
|
+
await recordOutputOutcomes(tx, rows, receivedAt);
|
|
246
251
|
});
|
|
247
252
|
} catch (error) {
|
|
248
253
|
if (!isFirstPartyAnalyticsDeliveryQueueMissingError(error)) throw error;
|
|
@@ -269,6 +274,7 @@ async function persistBigQueryRowsWithMigrationFallback(
|
|
|
269
274
|
}
|
|
270
275
|
await recordSessionEventIndex(tx, rows, receivedAt);
|
|
271
276
|
await recordSessionPerformance(tx, rows, receivedAt);
|
|
277
|
+
await recordOutputOutcomes(tx, rows, receivedAt);
|
|
272
278
|
});
|
|
273
279
|
try {
|
|
274
280
|
const result = await runWithRequestContext(
|
|
@@ -948,31 +954,29 @@ export async function recordAnalyticsEvents(
|
|
|
948
954
|
};
|
|
949
955
|
});
|
|
950
956
|
|
|
951
|
-
const backend = await getFirstPartyAnalyticsBackend({
|
|
952
|
-
userEmail: key.ownerEmail,
|
|
953
|
-
orgId: key.orgId ?? null,
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
if (rows.length && backend.sink === "dual") {
|
|
957
|
-
try {
|
|
958
|
-
await runWithRequestContext(
|
|
959
|
-
{
|
|
960
|
-
userEmail: key.ownerEmail,
|
|
961
|
-
orgId: key.orgId ?? undefined,
|
|
962
|
-
},
|
|
963
|
-
() => insertFirstPartyAnalyticsRows(rows, backend.table),
|
|
964
|
-
);
|
|
965
|
-
} catch (error) {
|
|
966
|
-
console.error(
|
|
967
|
-
"[first-party-analytics] BigQuery dual-write failed; retaining Postgres event:",
|
|
968
|
-
error,
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
957
|
let persistenceError: unknown = null;
|
|
974
958
|
if (rows.length) {
|
|
975
959
|
try {
|
|
960
|
+
const backend = await getFirstPartyAnalyticsBackend({
|
|
961
|
+
userEmail: key.ownerEmail,
|
|
962
|
+
orgId: key.orgId ?? null,
|
|
963
|
+
});
|
|
964
|
+
if (backend.sink === "dual") {
|
|
965
|
+
try {
|
|
966
|
+
await runWithRequestContext(
|
|
967
|
+
{
|
|
968
|
+
userEmail: key.ownerEmail,
|
|
969
|
+
orgId: key.orgId ?? undefined,
|
|
970
|
+
},
|
|
971
|
+
() => insertFirstPartyAnalyticsRows(rows, backend.table),
|
|
972
|
+
);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
console.error(
|
|
975
|
+
"[first-party-analytics] BigQuery dual-write failed; retaining Postgres event:",
|
|
976
|
+
error,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
976
980
|
if (backend.sink === "bigquery") {
|
|
977
981
|
await persistBigQueryRowsWithMigrationFallback(
|
|
978
982
|
db,
|
|
@@ -998,10 +1002,12 @@ export async function recordAnalyticsEvents(
|
|
|
998
1002
|
await upsertFirstPartyAnalyticsRollups(rows, tx);
|
|
999
1003
|
await recordSessionEventIndex(tx, rows, receivedAt);
|
|
1000
1004
|
await recordSessionPerformance(tx, rows, receivedAt);
|
|
1005
|
+
await recordOutputOutcomes(tx, rows, receivedAt);
|
|
1001
1006
|
});
|
|
1002
1007
|
}
|
|
1003
1008
|
} catch (error) {
|
|
1004
1009
|
persistenceError = error;
|
|
1010
|
+
await recordLostOutputBatch(db, rows, receivedAt);
|
|
1005
1011
|
}
|
|
1006
1012
|
}
|
|
1007
1013
|
if (rows.length && !persistenceError) {
|
|
@@ -1022,22 +1022,57 @@ const FUNNEL_EVENTS_CTE = `WITH auth_identity_bridge AS (
|
|
|
1022
1022
|
AND NULLIF(e.anonymous_id, '') IS NOT NULL
|
|
1023
1023
|
GROUP BY NULLIF(e.anonymous_id, '')
|
|
1024
1024
|
HAVING COUNT(DISTINCT NULLIF(e.properties::jsonb ->> 'auth_user_id', '')) <= 1
|
|
1025
|
+
), signup_session_identity AS (
|
|
1026
|
+
-- Visitors in the consent region send no anonymous id until they accept
|
|
1027
|
+
-- analytics cookies, so their pre-signup events reach the signup only
|
|
1028
|
+
-- through the session they share with it.
|
|
1029
|
+
SELECT NULLIF(e.session_id, '') AS session_id,
|
|
1030
|
+
MIN(NULLIF(e.properties::jsonb ->> 'auth_user_id', '')) AS signup_auth_user_id,
|
|
1031
|
+
MIN(
|
|
1032
|
+
CASE
|
|
1033
|
+
WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id
|
|
1034
|
+
WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key
|
|
1035
|
+
END
|
|
1036
|
+
) AS signup_user_email
|
|
1037
|
+
FROM analytics_events e
|
|
1038
|
+
WHERE e.event_name = 'signup'
|
|
1039
|
+
AND ${DASHBOARD_TIME_RANGE_FILTER}
|
|
1040
|
+
AND ${DASHBOARD_APP_FILTER}
|
|
1041
|
+
AND ${FIRST_PARTY_TEMPLATE_FILTER}
|
|
1042
|
+
AND NULLIF(e.session_id, '') IS NOT NULL
|
|
1043
|
+
GROUP BY NULLIF(e.session_id, '')
|
|
1044
|
+
-- A session with two signups can't say whose visit it was.
|
|
1045
|
+
HAVING COUNT(DISTINCT NULLIF(e.properties::jsonb ->> 'auth_user_id', '')) <= 1
|
|
1046
|
+
AND COUNT(
|
|
1047
|
+
DISTINCT CASE
|
|
1048
|
+
WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id
|
|
1049
|
+
WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key
|
|
1050
|
+
END
|
|
1051
|
+
) <= 1
|
|
1025
1052
|
), raw_funnel_events AS (
|
|
1026
1053
|
SELECT e.*,
|
|
1027
1054
|
COALESCE(
|
|
1028
1055
|
NULLIF(e.properties::jsonb ->> 'auth_user_id', ''),
|
|
1029
1056
|
si.signup_auth_user_id,
|
|
1030
1057
|
si.signup_user_email,
|
|
1058
|
+
ss.signup_auth_user_id,
|
|
1059
|
+
ss.signup_user_email,
|
|
1031
1060
|
NULLIF(e.user_id, ''),
|
|
1032
1061
|
NULLIF(e.anonymous_id, '')
|
|
1033
1062
|
) AS funnel_user_key,
|
|
1034
1063
|
COALESCE(
|
|
1035
1064
|
CASE WHEN NULLIF(e.user_id, '') LIKE '%@%.%' THEN e.user_id END,
|
|
1036
1065
|
CASE WHEN NULLIF(e.user_key, '') LIKE '%@%.%' THEN e.user_key END,
|
|
1037
|
-
si.signup_user_email
|
|
1066
|
+
si.signup_user_email,
|
|
1067
|
+
ss.signup_user_email
|
|
1038
1068
|
) AS funnel_user_email
|
|
1039
1069
|
FROM analytics_events e
|
|
1040
1070
|
LEFT JOIN signup_identity si ON si.anonymous_id = NULLIF(e.anonymous_id, '')
|
|
1071
|
+
LEFT JOIN signup_session_identity ss
|
|
1072
|
+
ON NULLIF(e.anonymous_id, '') IS NULL
|
|
1073
|
+
AND NULLIF(e.user_id, '') IS NULL
|
|
1074
|
+
AND NULLIF(e.properties::jsonb ->> 'auth_user_id', '') IS NULL
|
|
1075
|
+
AND ss.session_id = NULLIF(e.session_id, '')
|
|
1041
1076
|
WHERE ${DASHBOARD_TIME_RANGE_FILTER}
|
|
1042
1077
|
AND ${DASHBOARD_APP_FILTER}
|
|
1043
1078
|
AND ${FIRST_PARTY_TEMPLATE_FILTER}
|