@agent-native/core 0.122.1 → 0.122.3
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/README.md +1 -13
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +65 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +12 -0
- package/corpus/core/src/agent/run-store.ts +61 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +182 -0
- package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
- package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/corpus/core/src/integrations/plugin.ts +453 -35
- package/corpus/core/src/integrations/webhook-handler.ts +469 -11
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
- package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +12 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
- package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
- package/corpus/templates/analytics/server/plugins/db.ts +41 -1
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
- package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
- package/corpus/templates/content/app/root.tsx +12 -1
- package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/design/actions/edit-design.ts +66 -27
- package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +5 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
- package/corpus/templates/forms/actions/create-form.ts +5 -1
- package/corpus/templates/forms/actions/export-responses.ts +22 -11
- package/corpus/templates/forms/actions/update-form.ts +5 -1
- package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
- package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
- package/corpus/templates/slides/actions/import-file.ts +3 -4
- package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
- package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +63 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +7 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +7 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +49 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +134 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -0
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +99 -13
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
- package/dist/integrations/integration-campaign-recovery.js +94 -0
- package/dist/integrations/integration-campaign-recovery.js.map +1 -0
- package/dist/integrations/integration-campaigns-store.d.ts +121 -0
- package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
- package/dist/integrations/integration-campaigns-store.js +784 -0
- package/dist/integrations/integration-campaigns-store.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
- package/dist/integrations/integration-durable-dispatch.js +22 -3
- package/dist/integrations/integration-durable-dispatch.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +6 -0
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +325 -18
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +16 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +368 -13
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/production-agent.ts +65 -1
- package/src/agent/run-loop-with-resume.ts +12 -0
- package/src/agent/run-store.ts +61 -1
- package/src/integrations/a2a-continuation-processor.ts +182 -0
- package/src/integrations/a2a-continuations-store.ts +122 -12
- package/src/integrations/integration-campaign-recovery.ts +127 -0
- package/src/integrations/integration-campaigns-store.ts +1039 -0
- package/src/integrations/integration-durable-dispatch.ts +28 -3
- package/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/src/integrations/plugin.ts +453 -35
- package/src/integrations/webhook-handler.ts +469 -11
- package/src/server/builder-design-systems.ts +1 -1
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
ingestAnalyticsExceptionEvents,
|
|
9
9
|
type DerivedExceptionFields,
|
|
10
10
|
} from "./error-capture.js";
|
|
11
|
+
import {
|
|
12
|
+
firstPartyCacheKey,
|
|
13
|
+
withFirstPartyCache,
|
|
14
|
+
} from "./first-party-analytics-cache.js";
|
|
11
15
|
|
|
12
16
|
export interface AnalyticsScope {
|
|
13
17
|
userEmail: string;
|
|
@@ -29,6 +33,13 @@ export interface AnalyticsQueryResult {
|
|
|
29
33
|
schema: { name: string; type: string }[];
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
export interface AnalyticsQueryOptions {
|
|
37
|
+
/** Cache only callers with a stable dashboard-panel lifecycle. */
|
|
38
|
+
cache?: boolean;
|
|
39
|
+
/** Bound the database work for callers with a smaller delivery deadline. */
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
const MAX_EVENTS_PER_REQUEST = 100;
|
|
33
44
|
const MAX_QUERY_ROWS = 5_000;
|
|
34
45
|
const FIRST_PARTY_QUERY_TABLES = new Set([
|
|
@@ -636,16 +647,30 @@ function inferSchema(rows: Record<string, unknown>[]): {
|
|
|
636
647
|
export async function queryFirstPartyAnalytics(
|
|
637
648
|
sql: string,
|
|
638
649
|
scope: AnalyticsScope,
|
|
650
|
+
options: AnalyticsQueryOptions = {},
|
|
639
651
|
): Promise<AnalyticsQueryResult> {
|
|
640
652
|
validateFirstPartyAnalyticsSql(sql);
|
|
641
653
|
const scoped = scopedAnalyticsSql(sql, scope);
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
654
|
+
const wrappedSql = `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`;
|
|
655
|
+
const timeoutMs = Math.max(
|
|
656
|
+
1,
|
|
657
|
+
options.timeoutMs ?? FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS,
|
|
658
|
+
);
|
|
659
|
+
// The cache key is the fully scoped SQL + args, which already embeds
|
|
660
|
+
// org_id/owner_email (see scopeClause) — a cache hit can only ever return
|
|
661
|
+
// rows the same tenant was already entitled to query.
|
|
662
|
+
const cacheKey = firstPartyCacheKey(wrappedSql, scoped.args);
|
|
663
|
+
const compute = async (): Promise<AnalyticsQueryResult> => {
|
|
664
|
+
const exec = getDbExec();
|
|
665
|
+
const result = await exec.execute({
|
|
666
|
+
sql: wrappedSql,
|
|
667
|
+
args: scoped.args,
|
|
668
|
+
timeoutMs,
|
|
669
|
+
maxAttempts: 1,
|
|
670
|
+
});
|
|
671
|
+
const rows = result.rows as Record<string, unknown>[];
|
|
672
|
+
return { rows, schema: inferSchema(rows) };
|
|
673
|
+
};
|
|
674
|
+
if (!options.cache) return compute();
|
|
675
|
+
return withFirstPartyCache(cacheKey, wrappedSql, compute, { timeoutMs });
|
|
651
676
|
}
|
|
@@ -1,40 +1,47 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { getDialect } from "@agent-native/core/db";
|
|
3
4
|
import { recordChange } from "@agent-native/core/server";
|
|
4
5
|
import { and, desc, eq } from "drizzle-orm";
|
|
5
6
|
|
|
6
7
|
import { getDb, schema } from "../db/index.js";
|
|
7
8
|
import { repairCanonicalFirstPartyDashboardQueries } from "./canonical-first-party-dashboard-repair";
|
|
8
9
|
import { FIRST_PARTY_DASHBOARD_ID } from "./first-party-metric-catalog";
|
|
10
|
+
import { repairUnboundedFirstPartyPanels } from "./first-party-unbounded-panel-repair.js";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
updatedAt: schema.dashboards.updatedAt,
|
|
21
|
-
ownerEmail: schema.dashboards.ownerEmail,
|
|
22
|
-
orgId: schema.dashboards.orgId,
|
|
23
|
-
visibility: schema.dashboards.visibility,
|
|
24
|
-
})
|
|
25
|
-
.from(schema.dashboards)
|
|
26
|
-
.where(eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID));
|
|
27
|
-
if (!row || row.kind !== "sql" || typeof row.config !== "string") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
12
|
+
type DashboardRepairRow = {
|
|
13
|
+
id: string;
|
|
14
|
+
config: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
title: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
ownerEmail: string;
|
|
19
|
+
orgId: string | null;
|
|
20
|
+
visibility: string;
|
|
21
|
+
};
|
|
30
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Apply `repairFn` to one already-fetched dashboard row inside a transaction,
|
|
25
|
+
* guarded by an optimistic-concurrency fence on (config, updatedAt) so a
|
|
26
|
+
* concurrent human/agent edit always wins over the repair. Snapshots the
|
|
27
|
+
* pre-repair config into dashboard_revisions (bounded to the 50 most recent)
|
|
28
|
+
* before committing, so any repair is one click away from undo.
|
|
29
|
+
*/
|
|
30
|
+
async function applyRepairToDashboardRow(
|
|
31
|
+
row: DashboardRepairRow,
|
|
32
|
+
repairFn: (config: Record<string, unknown>) => {
|
|
33
|
+
config: Record<string, unknown>;
|
|
34
|
+
changed: boolean;
|
|
35
|
+
},
|
|
36
|
+
): Promise<boolean> {
|
|
37
|
+
const db = getDb() as any;
|
|
31
38
|
let config: Record<string, unknown>;
|
|
32
39
|
try {
|
|
33
40
|
config = JSON.parse(row.config) as Record<string, unknown>;
|
|
34
41
|
} catch {
|
|
35
42
|
return false;
|
|
36
43
|
}
|
|
37
|
-
const repaired =
|
|
44
|
+
const repaired = repairFn(config);
|
|
38
45
|
if (!repaired.changed) return false;
|
|
39
46
|
|
|
40
47
|
const repairedAt = new Date().toISOString();
|
|
@@ -49,7 +56,7 @@ export async function repairPersistedFirstPartyDashboardQueries(): Promise<boole
|
|
|
49
56
|
})
|
|
50
57
|
.where(
|
|
51
58
|
and(
|
|
52
|
-
eq(schema.dashboards.id,
|
|
59
|
+
eq(schema.dashboards.id, row.id),
|
|
53
60
|
eq(schema.dashboards.config, row.config),
|
|
54
61
|
eq(schema.dashboards.updatedAt, row.updatedAt),
|
|
55
62
|
),
|
|
@@ -104,9 +111,84 @@ export async function repairPersistedFirstPartyDashboardQueries(): Promise<boole
|
|
|
104
111
|
});
|
|
105
112
|
} catch (err) {
|
|
106
113
|
console.warn(
|
|
107
|
-
"[db]
|
|
114
|
+
"[db] Dashboard repair committed without a live change event:",
|
|
108
115
|
err instanceof Error ? err.message : err,
|
|
109
116
|
);
|
|
110
117
|
}
|
|
111
118
|
return true;
|
|
112
119
|
}
|
|
120
|
+
|
|
121
|
+
export async function repairPersistedFirstPartyDashboardQueries(): Promise<boolean> {
|
|
122
|
+
// guard:allow-unscoped — startup repair targets one fixed canonical dashboard
|
|
123
|
+
// and only replaces the exact shipped legacy SQL under an optimistic fence.
|
|
124
|
+
const db = getDb() as any;
|
|
125
|
+
const [row] = await db
|
|
126
|
+
.select({
|
|
127
|
+
id: schema.dashboards.id,
|
|
128
|
+
config: schema.dashboards.config,
|
|
129
|
+
kind: schema.dashboards.kind,
|
|
130
|
+
title: schema.dashboards.title,
|
|
131
|
+
updatedAt: schema.dashboards.updatedAt,
|
|
132
|
+
ownerEmail: schema.dashboards.ownerEmail,
|
|
133
|
+
orgId: schema.dashboards.orgId,
|
|
134
|
+
visibility: schema.dashboards.visibility,
|
|
135
|
+
})
|
|
136
|
+
.from(schema.dashboards)
|
|
137
|
+
.where(eq(schema.dashboards.id, FIRST_PARTY_DASHBOARD_ID));
|
|
138
|
+
if (!row || row.kind !== "sql" || typeof row.config !== "string") {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return applyRepairToDashboardRow(
|
|
142
|
+
row,
|
|
143
|
+
repairCanonicalFirstPartyDashboardQueries,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Scan every SQL dashboard (not just the one canonical dashboard above) for
|
|
149
|
+
* first-party panels whose SQL exactly matches a known-unbounded pattern —
|
|
150
|
+
* see first-party-unbounded-panel-repair.ts for how these were found (a
|
|
151
|
+
* full-org audit, 2026-07-25) and why exact-string matching, not a general
|
|
152
|
+
* SQL rewrite, is the safe way to fix panels this repair didn't author.
|
|
153
|
+
* Returns the number of dashboards actually changed.
|
|
154
|
+
*/
|
|
155
|
+
export async function repairUnboundedFirstPartyPanelsAcrossDashboards(): Promise<number> {
|
|
156
|
+
// guard:allow-unscoped — this is an org-wide startup repair: it may touch
|
|
157
|
+
// any dashboard's persisted panel SQL, but only ever replaces an exact
|
|
158
|
+
// known-bad SQL string under the same optimistic (config, updatedAt) fence
|
|
159
|
+
// used by the canonical repair above, so a concurrent edit always wins.
|
|
160
|
+
const db = getDb() as any;
|
|
161
|
+
const dialect = getDialect();
|
|
162
|
+
const rows = await db
|
|
163
|
+
.select({
|
|
164
|
+
id: schema.dashboards.id,
|
|
165
|
+
config: schema.dashboards.config,
|
|
166
|
+
kind: schema.dashboards.kind,
|
|
167
|
+
title: schema.dashboards.title,
|
|
168
|
+
updatedAt: schema.dashboards.updatedAt,
|
|
169
|
+
ownerEmail: schema.dashboards.ownerEmail,
|
|
170
|
+
orgId: schema.dashboards.orgId,
|
|
171
|
+
visibility: schema.dashboards.visibility,
|
|
172
|
+
})
|
|
173
|
+
.from(schema.dashboards)
|
|
174
|
+
.where(eq(schema.dashboards.kind, "sql"));
|
|
175
|
+
|
|
176
|
+
let repairedCount = 0;
|
|
177
|
+
for (const row of rows as DashboardRepairRow[]) {
|
|
178
|
+
if (typeof row.config !== "string") continue;
|
|
179
|
+
try {
|
|
180
|
+
const wasRepaired = await applyRepairToDashboardRow(row, (config) =>
|
|
181
|
+
repairUnboundedFirstPartyPanels(config, dialect),
|
|
182
|
+
);
|
|
183
|
+
if (wasRepaired) repairedCount += 1;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// One dashboard's malformed config or a lost optimistic-concurrency
|
|
186
|
+
// race must not block repairing every other dashboard.
|
|
187
|
+
console.warn(
|
|
188
|
+
`[db] Unbounded first-party panel repair failed for dashboard "${row.id}" (non-fatal):`,
|
|
189
|
+
err instanceof Error ? err.message : err,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return repairedCount;
|
|
194
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Dialect } from "@agent-native/core/db";
|
|
2
|
+
|
|
3
|
+
import type { DashboardPanelLike } from "./dashboard-time-scope.js";
|
|
4
|
+
|
|
5
|
+
const POSTGRES_DATE_BOUND =
|
|
6
|
+
"to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')";
|
|
7
|
+
const SQLITE_DATE_BOUND = "date('now', '-365 days')";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fixes for first-party dashboard panels found, via a full-org audit
|
|
11
|
+
* (2026-07-25), reading `analytics_events` with no date bound at all in some
|
|
12
|
+
* or all of their scan units — a full 6.7M-row table scan on every render.
|
|
13
|
+
* Unlike the id-keyed replacements in first-party-metric-catalog.ts (which
|
|
14
|
+
* repair one specific dashboard's known panel ids), these are matched purely
|
|
15
|
+
* by exact SQL text so the same fix applies wherever the identical broken
|
|
16
|
+
* query was cloned into a different dashboard under a different panel id.
|
|
17
|
+
* Each entry only changes the added bound (`AND event_date >= ...365 days`,
|
|
18
|
+
* or the exact same bound already live on the repaired canonical dashboard
|
|
19
|
+
* for the retention-cohort case) — never the query's selected columns,
|
|
20
|
+
* grouping, or business logic.
|
|
21
|
+
*/
|
|
22
|
+
export type UnboundedFirstPartyPanelFix = {
|
|
23
|
+
legacySql: string;
|
|
24
|
+
sql: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function dialectBoundedSql(sql: string, dialect: Dialect): string {
|
|
28
|
+
return dialect === "postgres"
|
|
29
|
+
? sql
|
|
30
|
+
: sql.split(POSTGRES_DATE_BOUND).join(SQLITE_DATE_BOUND);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const UNBOUNDED_FIRST_PARTY_PANEL_FIXES: readonly UnboundedFirstPartyPanelFix[] =
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
legacySql:
|
|
37
|
+
"SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY event_date ORDER BY date",
|
|
38
|
+
sql: "SELECT event_date AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY event_date ORDER BY date",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
legacySql:
|
|
42
|
+
"SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
|
|
43
|
+
sql: "SELECT COALESCE(NULLIF(signed_in, ''), 'unknown') AS signed_in, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY COALESCE(NULLIF(signed_in, ''), 'unknown') ORDER BY signed_in",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
legacySql:
|
|
47
|
+
"SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
|
|
48
|
+
sql: "SELECT COALESCE(NULLIF(app, ''), 'unknown') AS app, COUNT(*) AS count FROM analytics_events WHERE event_name = 'session status' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY COALESCE(NULLIF(app, ''), 'unknown') ORDER BY count DESC LIMIT 20",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
legacySql:
|
|
52
|
+
"SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli started'",
|
|
53
|
+
sql: "SELECT COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli started' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
legacySql:
|
|
57
|
+
"SELECT COUNT(DISTINCT anonymous_id) AS count FROM analytics_events WHERE event_name LIKE 'skills_cli %'",
|
|
58
|
+
sql: "SELECT COUNT(DISTINCT anonymous_id) AS count FROM analytics_events WHERE event_name LIKE 'skills_cli %' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
legacySql:
|
|
62
|
+
"SELECT COUNT(DISTINCT session_id) AS count FROM analytics_events WHERE event_name = 'skills_cli install completed'",
|
|
63
|
+
sql: "SELECT COUNT(DISTINCT session_id) AS count FROM analytics_events WHERE event_name = 'skills_cli install completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
legacySql:
|
|
67
|
+
"WITH started AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli started'), completed AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli install completed') SELECT CASE WHEN started.n = 0 THEN 0 ELSE ROUND(completed.n * 100.0 / started.n) END AS rate FROM started, completed",
|
|
68
|
+
sql: "WITH started AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli started' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), completed AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli install completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) SELECT CASE WHEN started.n = 0 THEN 0 ELSE ROUND(completed.n * 100.0 / started.n) END AS rate FROM started, completed",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
legacySql:
|
|
72
|
+
"WITH steps AS (SELECT 1 AS step_order, 'Started' AS step, COUNT(DISTINCT session_id) AS count FROM analytics_events WHERE event_name = 'skills_cli started' UNION ALL SELECT 2, 'Skills prompted', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills prompted' UNION ALL SELECT 3, 'Skills selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills selected' UNION ALL SELECT 4, 'Clients selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli clients selected' UNION ALL SELECT 5, 'Scope selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli scope selected' UNION ALL SELECT 6, 'Install completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli install completed' UNION ALL SELECT 7, 'Completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli completed') SELECT step, count FROM steps ORDER BY step_order",
|
|
73
|
+
sql: "WITH steps AS (SELECT 1 AS step_order, 'Started' AS step, COUNT(DISTINCT session_id) AS count FROM analytics_events WHERE event_name = 'skills_cli started' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 2, 'Skills prompted', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills prompted' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 3, 'Skills selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 4, 'Clients selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli clients selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 5, 'Scope selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli scope selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 6, 'Install completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli install completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 7, 'Completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) SELECT step, count FROM steps ORDER BY step_order",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
legacySql:
|
|
77
|
+
"SELECT trim(s) AS skill, COUNT(*) AS count FROM analytics_events, LATERAL unnest(string_to_array(properties::jsonb ->> 'selected', ',')) AS s WHERE event_name = 'skills_cli skills selected' AND (properties::jsonb ->> 'selected') <> '' GROUP BY trim(s) ORDER BY count DESC LIMIT 30",
|
|
78
|
+
sql: "SELECT trim(s) AS skill, COUNT(*) AS count FROM analytics_events, LATERAL unnest(string_to_array(properties::jsonb ->> 'selected', ',')) AS s WHERE event_name = 'skills_cli skills selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND (properties::jsonb ->> 'selected') <> '' GROUP BY trim(s) ORDER BY count DESC LIMIT 30",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
legacySql:
|
|
82
|
+
"SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli started' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
|
|
83
|
+
sql: "SELECT substr(timestamp, 1, 10) AS date, COUNT(*) AS count FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND event_name = 'skills_cli started' GROUP BY substr(timestamp, 1, 10) ORDER BY date",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
legacySql:
|
|
87
|
+
"SELECT COALESCE(NULLIF(properties::jsonb ->> 'cli', ''), 'unknown') AS cli, COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli started' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'cli', ''), 'unknown') ORDER BY count DESC",
|
|
88
|
+
sql: "SELECT COALESCE(NULLIF(properties::jsonb ->> 'cli', ''), 'unknown') AS cli, COUNT(*) AS count FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND event_name = 'skills_cli started' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'cli', ''), 'unknown') ORDER BY count DESC",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
legacySql:
|
|
92
|
+
"SELECT COALESCE(NULLIF(properties::jsonb ->> 'platform', ''), 'unknown') AS platform, COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli started' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'platform', ''), 'unknown') ORDER BY count DESC",
|
|
93
|
+
sql: "SELECT COALESCE(NULLIF(properties::jsonb ->> 'platform', ''), 'unknown') AS platform, COUNT(*) AS count FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND event_name = 'skills_cli started' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'platform', ''), 'unknown') ORDER BY count DESC",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
legacySql:
|
|
97
|
+
"SELECT COALESCE(NULLIF(properties::jsonb ->> 'scope', ''), 'unknown') AS scope, COUNT(*) AS count FROM analytics_events WHERE event_name = 'skills_cli scope selected' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'scope', ''), 'unknown') ORDER BY count DESC",
|
|
98
|
+
sql: "SELECT COALESCE(NULLIF(properties::jsonb ->> 'scope', ''), 'unknown') AS scope, COUNT(*) AS count FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND event_name = 'skills_cli scope selected' GROUP BY COALESCE(NULLIF(properties::jsonb ->> 'scope', ''), 'unknown') ORDER BY count DESC",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
legacySql:
|
|
102
|
+
"SELECT trim(c) AS client, COUNT(*) AS count FROM analytics_events, LATERAL unnest(string_to_array(properties::jsonb ->> 'clients', ',')) AS c WHERE event_name = 'skills_cli clients selected' AND (properties::jsonb ->> 'clients') <> '' GROUP BY trim(c) ORDER BY count DESC LIMIT 30",
|
|
103
|
+
sql: "SELECT trim(c) AS client, COUNT(*) AS count FROM analytics_events, LATERAL unnest(string_to_array(properties::jsonb ->> 'clients', ',')) AS c WHERE event_name = 'skills_cli clients selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') AND (properties::jsonb ->> 'clients') <> '' GROUP BY trim(c) ORDER BY count DESC LIMIT 30",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
legacySql:
|
|
107
|
+
"WITH steps AS (SELECT 1 AS step_order, 'Started' AS step, COUNT(DISTINCT session_id) AS reached FROM analytics_events WHERE event_name = 'skills_cli started' UNION ALL SELECT 2, 'Skills prompted', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills prompted' UNION ALL SELECT 3, 'Skills selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills selected' UNION ALL SELECT 4, 'Clients selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli clients selected' UNION ALL SELECT 5, 'Scope selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli scope selected' UNION ALL SELECT 6, 'Install completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli install completed' UNION ALL SELECT 7, 'Completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli completed'), start_total AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli started') SELECT steps.step, steps.reached, CASE WHEN start_total.n = 0 THEN 0 ELSE ROUND(steps.reached * 100.0 / start_total.n) END AS pct_of_start FROM steps, start_total ORDER BY steps.step_order",
|
|
108
|
+
sql: "WITH steps AS (SELECT 1 AS step_order, 'Started' AS step, COUNT(DISTINCT session_id) AS reached FROM analytics_events WHERE event_name = 'skills_cli started' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 2, 'Skills prompted', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills prompted' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 3, 'Skills selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli skills selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 4, 'Clients selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli clients selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 5, 'Scope selected', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli scope selected' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 6, 'Install completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli install completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') UNION ALL SELECT 7, 'Completed', COUNT(DISTINCT session_id) FROM analytics_events WHERE event_name = 'skills_cli completed' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), start_total AS (SELECT COUNT(DISTINCT session_id) AS n FROM analytics_events WHERE event_name = 'skills_cli started' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')) SELECT steps.step, steps.reached, CASE WHEN start_total.n = 0 THEN 0 ELSE ROUND(steps.reached * 100.0 / start_total.n) END AS pct_of_start FROM steps, start_total ORDER BY steps.step_order",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
legacySql: "SELECT COUNT(*) AS value FROM analytics_events",
|
|
112
|
+
sql: "SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
legacySql:
|
|
116
|
+
"SELECT COUNT(DISTINCT session_id) AS value FROM analytics_events",
|
|
117
|
+
sql: "SELECT COUNT(DISTINCT session_id) AS value FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
legacySql:
|
|
121
|
+
"SELECT event_name AS label, COUNT(*) AS value FROM analytics_events GROUP BY event_name ORDER BY value DESC LIMIT 8",
|
|
122
|
+
sql: "SELECT event_name AS label, COUNT(*) AS value FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY event_name ORDER BY value DESC LIMIT 8",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
legacySql:
|
|
126
|
+
"SELECT CASE WHEN signed_in = 'true' THEN 'Signed In' ELSE 'Anonymous' END AS user_type, COUNT(*) AS events FROM analytics_events GROUP BY signed_in",
|
|
127
|
+
sql: "SELECT CASE WHEN signed_in = 'true' THEN 'Signed In' ELSE 'Anonymous' END AS user_type, COUNT(*) AS events FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') GROUP BY signed_in",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
legacySql:
|
|
131
|
+
"SELECT timestamp, event_name, app, signed_in FROM analytics_events ORDER BY timestamp DESC LIMIT 50",
|
|
132
|
+
sql: "SELECT timestamp, event_name, app, signed_in FROM analytics_events WHERE event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD') ORDER BY timestamp DESC LIMIT 50",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
legacySql:
|
|
136
|
+
"WITH base AS (SELECT NULLIF(user_key, '') AS user_key, event_date AS event_date, user_id FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '14 days', 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= to_char(a.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= 5 ORDER BY cs.date, p.period",
|
|
137
|
+
sql: "WITH base AS (SELECT NULLIF(user_key, '') AS user_key, event_date AS event_date, user_id FROM analytics_events WHERE event_name = 'session status' AND signed_in = 'true' AND NULLIF(user_key, '') IS NOT NULL AND lower(COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')) <> 'docs' AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io')) AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= to_char(CURRENT_DATE - INTERVAL '14 days', 'YYYY-MM-DD') AND (cohort_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND cohort_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= to_char(a.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= 5 ORDER BY cs.date, p.period",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
legacySql:
|
|
141
|
+
"WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY event_date) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT event_date AS date, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'signup' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template",
|
|
142
|
+
sql: "WITH signup_events AS (SELECT event_date AS date, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'signup' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(d, 'YYYY-MM-DD') AS date FROM bounds, generate_series(bounds.start_date, bounds.end_date, INTERVAL '1 day') AS d WHERE bounds.start_date IS NOT NULL), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template",
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
export function repairUnboundedFirstPartyPanels(
|
|
147
|
+
config: Record<string, unknown>,
|
|
148
|
+
dialect: Dialect = "postgres",
|
|
149
|
+
): { config: Record<string, unknown>; changed: boolean } {
|
|
150
|
+
if (!Array.isArray(config.panels)) return { config, changed: false };
|
|
151
|
+
|
|
152
|
+
const fixBySql = new Map(
|
|
153
|
+
UNBOUNDED_FIRST_PARTY_PANEL_FIXES.map((fix) => [fix.legacySql, fix.sql]),
|
|
154
|
+
);
|
|
155
|
+
let changed = false;
|
|
156
|
+
const panels = config.panels.map((rawPanel) => {
|
|
157
|
+
if (!rawPanel || typeof rawPanel !== "object") return rawPanel;
|
|
158
|
+
const panel = rawPanel as DashboardPanelLike & Record<string, unknown>;
|
|
159
|
+
if (panel.source !== "first-party" || typeof panel.sql !== "string") {
|
|
160
|
+
return rawPanel;
|
|
161
|
+
}
|
|
162
|
+
const fixedSql = fixBySql.get(panel.sql);
|
|
163
|
+
if (fixedSql === undefined) return rawPanel;
|
|
164
|
+
changed = true;
|
|
165
|
+
return { ...panel, sql: dialectBoundedSql(fixedSql, dialect) };
|
|
166
|
+
});
|
|
167
|
+
if (!changed) return { config, changed: false };
|
|
168
|
+
return { config: { ...config, panels }, changed: true };
|
|
169
|
+
}
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
// startup so the dashboard / analysis share actions know where to dispatch.
|
|
9
9
|
import "../db/index.js";
|
|
10
10
|
import * as schema from "../db/schema.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
repairPersistedFirstPartyDashboardQueries,
|
|
13
|
+
repairUnboundedFirstPartyPanelsAcrossDashboards,
|
|
14
|
+
} from "../lib/first-party-dashboard-repair.js";
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Every Drizzle table exported from schema.ts. Filters out type-only and
|
|
@@ -1284,6 +1287,29 @@ const runAnalyticsMigrations = runMigrations(
|
|
|
1284
1287
|
ALTER TABLE dashboard_report_subscriptions ADD COLUMN IF NOT EXISTS last_capture_error TEXT;
|
|
1285
1288
|
`,
|
|
1286
1289
|
},
|
|
1290
|
+
// First-party dashboard panel result cache. Same shape/pattern as
|
|
1291
|
+
// bigquery_cache above, short TTL (set in first-party-analytics-cache.ts)
|
|
1292
|
+
// since this is the app's own live data, not an immutable warehouse
|
|
1293
|
+
// result. See first-party-analytics-cache.ts for why this exists: panel
|
|
1294
|
+
// queries had no cache at all, so every dashboard render and every daily
|
|
1295
|
+
// report screenshot recomputed from scratch and stacked concurrent load
|
|
1296
|
+
// on the same rows, which is what was blowing report/panel timeouts.
|
|
1297
|
+
{
|
|
1298
|
+
version: 124,
|
|
1299
|
+
name: "first-party-analytics-cache-table",
|
|
1300
|
+
sql: `CREATE TABLE IF NOT EXISTS first_party_analytics_cache (
|
|
1301
|
+
key TEXT PRIMARY KEY,
|
|
1302
|
+
sql TEXT NOT NULL,
|
|
1303
|
+
result TEXT NOT NULL,
|
|
1304
|
+
created_at TEXT NOT NULL,
|
|
1305
|
+
expires_at TEXT NOT NULL
|
|
1306
|
+
)`,
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
version: 125,
|
|
1310
|
+
name: "first-party-analytics-cache-expires-idx",
|
|
1311
|
+
sql: `CREATE INDEX IF NOT EXISTS first_party_analytics_cache_expires_at_idx ON first_party_analytics_cache (expires_at)`,
|
|
1312
|
+
},
|
|
1287
1313
|
],
|
|
1288
1314
|
{ table: "analytics_migrations" },
|
|
1289
1315
|
);
|
|
@@ -1312,6 +1338,20 @@ export default async (nitroApp: any): Promise<void> => {
|
|
|
1312
1338
|
err instanceof Error ? err.message : err,
|
|
1313
1339
|
);
|
|
1314
1340
|
}
|
|
1341
|
+
try {
|
|
1342
|
+
const repairedCount =
|
|
1343
|
+
await repairUnboundedFirstPartyPanelsAcrossDashboards();
|
|
1344
|
+
if (repairedCount > 0) {
|
|
1345
|
+
console.info(
|
|
1346
|
+
`[db] Repaired ${repairedCount} dashboard(s) with unbounded first-party panel SQL.`,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
console.warn(
|
|
1351
|
+
"[db] Failed to repair unbounded first-party panels across dashboards (non-fatal):",
|
|
1352
|
+
err instanceof Error ? err.message : err,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1315
1355
|
try {
|
|
1316
1356
|
const summary = await ensureAdditiveColumns({
|
|
1317
1357
|
db: getDbExec(),
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// These three must stay ordered with headroom (DB timeout < action timeout <
|
|
2
|
+
// page-ready timeout) so the innermost layer's own, more specific error
|
|
3
|
+
// surfaces before an outer layer gives up for an unrelated reason.
|
|
4
|
+
export const FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS = 45_000;
|
|
5
|
+
export const DASHBOARD_REPORT_ACTION_TIMEOUT_MS = 50_000;
|
|
6
|
+
export const DASHBOARD_REPORT_READY_TIMEOUT_MS = 75_000;
|
|
@@ -86,6 +86,7 @@ import { cn } from "@/lib/utils";
|
|
|
86
86
|
import {
|
|
87
87
|
documentBodyHydrationIsPending,
|
|
88
88
|
isEffectivelyEmptyDocumentContent,
|
|
89
|
+
newDocumentPageChoiceIsDisabled,
|
|
89
90
|
} from "./body-hydration";
|
|
90
91
|
import { BuilderBodySyncingNotice } from "./BuilderBodySyncingNotice";
|
|
91
92
|
import type { CommentTextAnchor } from "./comment-anchors";
|
|
@@ -1807,9 +1808,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1807
1808
|
type="button"
|
|
1808
1809
|
variant="outline"
|
|
1809
1810
|
className="justify-start gap-2"
|
|
1810
|
-
disabled={
|
|
1811
|
-
|
|
1812
|
-
|
|
1811
|
+
disabled={newDocumentPageChoiceIsDisabled({
|
|
1812
|
+
canEdit,
|
|
1813
|
+
bodyHydrationPending,
|
|
1814
|
+
databaseCreationPending:
|
|
1815
|
+
createDatabase.isPending,
|
|
1816
|
+
})}
|
|
1813
1817
|
onClick={handleChoosePage}
|
|
1814
1818
|
>
|
|
1815
1819
|
<IconFileText />
|
|
@@ -69,6 +69,16 @@ export function documentBodyHydrationIsPending(
|
|
|
69
69
|
return builderBodyHydrationIsPending(hydration);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export function newDocumentPageChoiceIsDisabled(args: {
|
|
73
|
+
canEdit: boolean;
|
|
74
|
+
bodyHydrationPending: boolean;
|
|
75
|
+
databaseCreationPending: boolean;
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
!args.canEdit || args.bodyHydrationPending || args.databaseCreationPending
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
export function previewBodyHydrationIsPending(args: {
|
|
73
83
|
item: Pick<ContentDatabaseItem, "bodyHydration" | "document">;
|
|
74
84
|
document: Pick<Document, "content" | "databaseMembership"> | null | undefined;
|
|
@@ -49,7 +49,11 @@ import {
|
|
|
49
49
|
useRouteLoaderData,
|
|
50
50
|
useRouteError,
|
|
51
51
|
} from "react-router";
|
|
52
|
-
import type {
|
|
52
|
+
import type {
|
|
53
|
+
LinksFunction,
|
|
54
|
+
LoaderFunctionArgs,
|
|
55
|
+
ShouldRevalidateFunctionArgs,
|
|
56
|
+
} from "react-router";
|
|
53
57
|
|
|
54
58
|
// Styled sonner wrapper — passed via AppProviders `toaster` prop to avoid duplicate.
|
|
55
59
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
|
@@ -105,6 +109,13 @@ export async function loader({
|
|
|
105
109
|
};
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
export function shouldRevalidate({
|
|
113
|
+
defaultShouldRevalidate,
|
|
114
|
+
formMethod,
|
|
115
|
+
}: ShouldRevalidateFunctionArgs) {
|
|
116
|
+
return formMethod ? defaultShouldRevalidate : false;
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
// Pass args to match content's 3-way theme-cycle UX (no disableTransitionOnChange).
|
|
109
120
|
const THEME_INIT_SCRIPT = getThemeInitScript("system", true);
|
|
110
121
|
|
|
@@ -14,7 +14,7 @@ import { defineConfig, devices } from "@playwright/test";
|
|
|
14
14
|
*/
|
|
15
15
|
export default defineConfig({
|
|
16
16
|
testDir: ".",
|
|
17
|
-
testMatch: /(registry-blocks|local-files)\.spec\.ts/,
|
|
17
|
+
testMatch: /(registry-blocks|local-files|database-preview-menu)\.spec\.ts/,
|
|
18
18
|
fullyParallel: true,
|
|
19
19
|
workers: process.env.CI ? 2 : 3,
|
|
20
20
|
retries: 2,
|