Skip to content
Merged
4 changes: 2 additions & 2 deletions templates/analytics/app/components/layout/Layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ describe("Analytics layout sidebar route policy", () => {
expect(source).toContain(
'className="min-h-0 min-w-0 flex flex-1 flex-col space-y-0.5 overflow-x-hidden overflow-y-auto px-2 py-3"',
);
expect(source).toContain("<AppSidebarHeader");
expect(source).toContain("<AppSidebarFooter");
expect(source).toContain(
'className="mt-3 shrink-0 min-w-0 space-y-0.5 border-t border-border/70 px-2 pt-3"',
);
expect(source).toContain("<AppSidebarHeader");
expect(source).toContain("<AppSidebarFooter");
expect(source).not.toContain(
'className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden py-2"',
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: fixed
date: 2026-09-10
---

BigQuery active-user charts now show the full selected history.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ SELECT date, template, visitors
FROM wau
ORDER BY date, template`;

const MALFORMED_FIRST_PARTY_BIGQUERY_WAU_SQL =
FIRST_PARTY_BIGQUERY_WAU_SQL.replace(
"WHEN '{{timeRange}}' = '7d'",
"WHEN '{{timeRange}}' = '{{timeRange}}'",
);

function isMalformedFirstPartyBigQueryWauSql(sql: string): boolean {
return sql.trim() === MALFORMED_FIRST_PARTY_BIGQUERY_WAU_SQL.trim();
}

export function repairFirstPartyBigQueryDashboardQueries(
config: Record<string, unknown>,
): { config: Record<string, unknown>; changed: boolean } {
Expand All @@ -86,7 +96,8 @@ export function repairFirstPartyBigQueryDashboardQueries(
panel.id !== "wau-over-time" ||
panel.source !== "bigquery" ||
typeof panel.sql !== "string" ||
panel.sql.trim() !== ""
(panel.sql.trim() !== "" &&
!isMalformedFirstPartyBigQueryWauSql(panel.sql))
) {
return rawPanel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,95 @@ describe("repairPersistedFirstPartyDashboardQueries", () => {
expect(panels[2].sql).toBe("");
});

it("repairs the malformed non-empty BigQuery wau query", async () => {
const weekly = requiredFirstPartyPanel("wau-over-time");
const malformedSql = FIRST_PARTY_BIGQUERY_WAU_SQL.replace(
"WHEN '{{timeRange}}' = '7d'",
"WHEN '{{timeRange}}' = '{{timeRange}}'",
);
const row = legacyRow({
id: FIRST_PARTY_BIGQUERY_DASHBOARD_ID,
config: JSON.stringify({
panels: [
{
...weekly,
source: "bigquery",
sql: malformedSql,
},
],
}),
});
const mocks = createDb(row);
dbMocks.getDb.mockReturnValue(mocks.db);

await expect(repairPersistedFirstPartyDashboardQueries()).resolves.toBe(
true,
);

const updateCalls = mocks.updateSet.mock.calls as unknown as Array<
[{ config: string }]
>;
expect(JSON.parse(updateCalls[0]![0].config).panels[0].sql).toBe(
FIRST_PARTY_BIGQUERY_WAU_SQL,
);
});

it("preserves a customized malformed-looking BigQuery wau query", async () => {
const weekly = requiredFirstPartyPanel("wau-over-time");
const customizedSql = FIRST_PARTY_BIGQUERY_WAU_SQL.replace(
"WHEN '{{timeRange}}' = '7d'",
"WHEN '{{timeRange}}' = '{{timeRange}}'",
).replace("ORDER BY date, template", "ORDER BY template, date");
const row = legacyRow({
id: FIRST_PARTY_BIGQUERY_DASHBOARD_ID,
config: JSON.stringify({
panels: [
{
...weekly,
source: "bigquery",
sql: customizedSql,
},
],
}),
});
const mocks = createDb(row);
dbMocks.getDb.mockReturnValue(mocks.db);

await expect(repairPersistedFirstPartyDashboardQueries()).resolves.toBe(
false,
);

expect(mocks.update).not.toHaveBeenCalled();
});

it("preserves a malformed-looking query when a SQL literal changes", async () => {
const weekly = requiredFirstPartyPanel("wau-over-time");
const customizedSql = FIRST_PARTY_BIGQUERY_WAU_SQL.replace(
"WHEN '{{timeRange}}' = '7d'",
"WHEN '{{timeRange}}' = '{{timeRange}}'",
).replace("'session status'", "'session status'");
const row = legacyRow({
id: FIRST_PARTY_BIGQUERY_DASHBOARD_ID,
config: JSON.stringify({
panels: [
{
...weekly,
source: "bigquery",
sql: customizedSql,
},
],
}),
});
const mocks = createDb(row);
dbMocks.getDb.mockReturnValue(mocks.db);

await expect(repairPersistedFirstPartyDashboardQueries()).resolves.toBe(
false,
);

expect(mocks.update).not.toHaveBeenCalled();
});

it("repairs the deployed materialized one-day retention panel during startup", async () => {
const retention = requiredFirstPartyPanel("one-day-retention-by-template");
const row = legacyRow({
Expand Down
Loading