diff --git a/app/analytics/__tests__/query.test.ts b/app/analytics/__tests__/query.test.ts index eb3b005b..fc66eea1 100644 --- a/app/analytics/__tests__/query.test.ts +++ b/app/analytics/__tests__/query.test.ts @@ -86,6 +86,7 @@ describe("AnalyticsEngineAPI", () => { "example.com", "DAY", new Date("2024-01-11 00:00:00"), // local time (because tz also passed) + new Date(), "America/New_York", ); @@ -106,6 +107,7 @@ describe("AnalyticsEngineAPI", () => { "example.com", "DAY", new Date("2024-01-13 00:00:00"), // local time (because tz also passed) + new Date(), "America/New_York", ); expect(result2).toEqual([ @@ -148,6 +150,7 @@ describe("AnalyticsEngineAPI", () => { "example.com", "HOUR", new Date("2024-01-17 05:00:00"), // local time (because tz also passed) + new Date(), "America/New_York", ); @@ -323,7 +326,7 @@ describe("AnalyticsEngineAPI", () => { "double1 as isVisitor, " + "double2 as isVisit, " + "SUM(_sample_interval) as count " + - "FROM metricsDataset WHERE timestamp > NOW() - INTERVAL '7' DAY AND blob8 = 'example.com' AND blob4 = 'CA' " + + "FROM metricsDataset WHERE timestamp >= NOW() - INTERVAL '7' DAY AND timestamp < NOW() AND blob8 = 'example.com' AND blob4 = 'CA' " + "GROUP BY blob4, double1, double2 " + "ORDER BY count DESC LIMIT 10", ); @@ -345,17 +348,41 @@ describe("intervalToSql", () => { // test intervalToSql test("should return the proper sql interval for 1d, 30d, 90d, etc (days)", () => { - expect(intervalToSql("1d")).toBe("NOW() - INTERVAL '1' DAY"); - expect(intervalToSql("30d")).toBe("NOW() - INTERVAL '30' DAY"); - expect(intervalToSql("90d")).toBe("NOW() - INTERVAL '90' DAY"); + expect(intervalToSql("1d")).toStrictEqual({ + startIntervalSql: "NOW() - INTERVAL '1' DAY", + endIntervalSql: "NOW()", + }); + expect(intervalToSql("30d")).toStrictEqual({ + startIntervalSql: "NOW() - INTERVAL '30' DAY", + endIntervalSql: "NOW()", + }); + expect(intervalToSql("90d")).toStrictEqual({ + startIntervalSql: "NOW() - INTERVAL '90' DAY", + endIntervalSql: "NOW()", + }); }); test("should return the proper tz-adjusted sql interval for 'today'", () => { - expect(intervalToSql("today", "America/New_York")).toBe( - "toDateTime('2024-04-29 04:00:00')", - ); - expect(intervalToSql("today", "America/Los_Angeles")).toBe( - "toDateTime('2024-04-29 07:00:00')", + expect(intervalToSql("today", "America/New_York")).toStrictEqual({ + startIntervalSql: "toDateTime('2024-04-29 04:00:00')", + endIntervalSql: "NOW()", + }); + expect(intervalToSql("today", "America/Los_Angeles")).toStrictEqual({ + startIntervalSql: "toDateTime('2024-04-29 07:00:00')", + endIntervalSql: "NOW()", + }); + }); + + test("should return the proper tz-adjusted sql interval for 'yesterday'", () => { + expect(intervalToSql("yesterday", "America/New_York")).toStrictEqual({ + startIntervalSql: "toDateTime('2024-04-28 04:00:00')", + endIntervalSql: "toDateTime('2024-04-29 04:00:00')", + }); + expect(intervalToSql("yesterday", "America/Los_Angeles")).toStrictEqual( + { + startIntervalSql: "toDateTime('2024-04-28 07:00:00')", + endIntervalSql: "toDateTime('2024-04-29 07:00:00')", + }, ); }); }); diff --git a/app/analytics/query.ts b/app/analytics/query.ts index 3966b292..3e107587 100644 --- a/app/analytics/query.ts +++ b/app/analytics/query.ts @@ -45,22 +45,30 @@ function accumulateCountsFromRowResult( } export function intervalToSql(interval: string, tz?: string) { - let intervalSql = ""; + let startIntervalSql = ""; + let endIntervalSql = ""; switch (interval) { case "today": // example: toDateTime('2024-01-07 00:00:00', 'America/New_York') - intervalSql = `toDateTime('${dayjs().tz(tz).startOf("day").utc().format("YYYY-MM-DD HH:mm:ss")}')`; + startIntervalSql = `toDateTime('${dayjs().tz(tz).startOf("day").utc().format("YYYY-MM-DD HH:mm:ss")}')`; + endIntervalSql = "NOW()"; + break; + case "yesterday": + startIntervalSql = `toDateTime('${dayjs().tz(tz).startOf("day").utc().subtract(1, "day").format("YYYY-MM-DD HH:mm:ss")}')`; + endIntervalSql = `toDateTime('${dayjs().tz(tz).startOf("day").utc().format("YYYY-MM-DD HH:mm:ss")}')`; break; case "1d": case "7d": case "30d": case "90d": - intervalSql = `NOW() - INTERVAL '${interval.split("d")[0]}' DAY`; + startIntervalSql = `NOW() - INTERVAL '${interval.split("d")[0]}' DAY`; + endIntervalSql = "NOW()"; break; default: - intervalSql = `NOW() - INTERVAL '1' DAY`; + startIntervalSql = `NOW() - INTERVAL '1' DAY`; + endIntervalSql = "NOW()"; } - return intervalSql; + return { startIntervalSql, endIntervalSql }; } /** @@ -77,6 +85,7 @@ export function intervalToSql(interval: string, tz?: string) { function generateEmptyRowsOverInterval( intervalType: "DAY" | "HOUR", startDateTime: Date, + endDateTime: Date, tz?: string, ): { [key: string]: number } { if (!tz) { @@ -97,7 +106,7 @@ function generateEmptyRowsOverInterval( // out how to get vitest/mock dates to recreate DST changes. // See: https://github.com/benvinegar/counterscale/pull/62 - while (startDateTime.getTime() < Date.now()) { + while (startDateTime.getTime() < endDateTime.getTime()) { const key = dayjs(startDateTime).utc().format("YYYY-MM-DD HH:mm:ss"); initialRows[key] = 0; @@ -173,6 +182,7 @@ export class AnalyticsEngineAPI { siteId: string, intervalType: "DAY" | "HOUR", startDateTime: Date, // start date/time in local timezone + endDateTime: Date, // end date/time in local timezone tz?: string, // local timezone filters: SearchFilters = {}, ) { @@ -190,6 +200,7 @@ export class AnalyticsEngineAPI { const initialRows = generateEmptyRowsOverInterval( intervalType, startDateTime, + endDateTime, tz, ); @@ -206,6 +217,7 @@ export class AnalyticsEngineAPI { // and merge them with the results. const localStartTime = dayjs(startDateTime).tz(tz).utc(); + const localEndTime = dayjs(endDateTime).tz(tz).utc(); const query = ` SELECT SUM(_sample_interval) as count, @@ -215,9 +227,9 @@ export class AnalyticsEngineAPI { /* output as UTC */ toDateTime(_bucket, 'Etc/UTC') as bucket - FROM metricsDataset - WHERE timestamp > toDateTime('${localStartTime.format("YYYY-MM-DD HH:mm:ss")}') + WHERE timestamp >= toDateTime('${localStartTime.format("YYYY-MM-DD HH:mm:ss")}') + AND timestamp < toDateTime('${localEndTime.format("YYYY-MM-DD HH:mm:ss")}') AND ${ColumnMappings.siteId} = '${siteId}' ${filterStr} GROUP BY _bucket @@ -279,7 +291,10 @@ export class AnalyticsEngineAPI { // defaults to 1 day if not specified const siteIdColumn = ColumnMappings["siteId"]; - const intervalSql = intervalToSql(interval, tz); + const { startIntervalSql, endIntervalSql } = intervalToSql( + interval, + tz, + ); const filterStr = filtersToSql(filters); @@ -288,7 +303,7 @@ export class AnalyticsEngineAPI { ${ColumnMappings.newVisitor} as isVisitor, ${ColumnMappings.newSession} as isVisit FROM metricsDataset - WHERE timestamp > ${intervalSql} + WHERE timestamp >= ${startIntervalSql} AND timestamp < ${endIntervalSql} ${filterStr} AND ${siteIdColumn} = '${siteId}' GROUP BY isVisitor, isVisit @@ -341,7 +356,10 @@ export class AnalyticsEngineAPI { page: number = 1, limit: number = 10, ) { - const intervalSql = intervalToSql(interval, tz); + const { startIntervalSql, endIntervalSql } = intervalToSql( + interval, + tz, + ); const filterStr = filtersToSql(filters); @@ -349,7 +367,7 @@ export class AnalyticsEngineAPI { const query = ` SELECT ${_column}, SUM(_sample_interval) as count FROM metricsDataset - WHERE timestamp > ${intervalSql} + WHERE timestamp >= ${startIntervalSql} AND timestamp < ${endIntervalSql} AND ${ColumnMappings.newVisitor} = 1 AND ${ColumnMappings.siteId} = '${siteId}' ${filterStr} @@ -405,7 +423,10 @@ export class AnalyticsEngineAPI { page: number = 1, limit: number = 10, ): Promise> { - const intervalSql = intervalToSql(interval, tz); + const { startIntervalSql, endIntervalSql } = intervalToSql( + interval, + tz, + ); const filterStr = filtersToSql(filters); @@ -416,7 +437,7 @@ export class AnalyticsEngineAPI { ${ColumnMappings.newSession} as isVisit, SUM(_sample_interval) as count FROM metricsDataset - WHERE timestamp > ${intervalSql} + WHERE timestamp >= ${startIntervalSql} AND timestamp < ${endIntervalSql} AND ${ColumnMappings.siteId} = '${siteId}' ${filterStr} GROUP BY ${_column}, ${ColumnMappings.newVisitor}, ${ColumnMappings.newSession} @@ -584,13 +605,16 @@ export class AnalyticsEngineAPI { limit = limit || 10; - const intervalSql = intervalToSql(interval, tz); + const { startIntervalSql, endIntervalSql } = intervalToSql( + interval, + tz, + ); const query = ` SELECT SUM(_sample_interval) as count, ${ColumnMappings.siteId} as siteId FROM metricsDataset - WHERE timestamp > ${intervalSql} + WHERE timestamp >= ${startIntervalSql} AND timestamp < ${endIntervalSql} GROUP BY siteId ORDER BY count DESC LIMIT ${limit} diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 13d8217f..d55b3b09 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -95,6 +95,7 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { let intervalType: "DAY" | "HOUR" = "DAY"; switch (interval) { case "today": + case "yesterday": case "1d": intervalType = "HOUR"; break; @@ -107,8 +108,12 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { // get start date in the past by subtracting interval * type let localDateTime = dayjs().utc(); + let localEndDateTime: dayjs.Dayjs | undefined; if (interval === "today") { localDateTime = localDateTime.tz(tz).startOf("day"); + } else if (interval === "yesterday") { + localDateTime = localDateTime.tz(tz).startOf("day").subtract(1, "day"); + localEndDateTime = localDateTime.endOf("day").add(2, "ms"); } else { const daysAgo = Number(interval.split("d")[0]); if (intervalType === "DAY") { @@ -123,10 +128,13 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => { } } + if (!localEndDateTime) localEndDateTime = dayjs().utc().tz(tz); + const viewsGroupedByInterval = analyticsEngine.getViewsGroupedByInterval( actualSiteId, intervalType, localDateTime.toDate(), + localEndDateTime.toDate(), tz, filters, ); @@ -249,6 +257,7 @@ export default function Dashboard() { Today + Yesterday 24 hours 7 days 30 days diff --git a/package-lock.json b/package-lock.json index 96906e62..bccb67c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "counterscale", - "version": "2.0.0-beta.1", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "counterscale", - "version": "2.0.0-beta.1", + "version": "2.0.0", "dependencies": { "@cloudflare/kv-asset-handler": "^0.3.3", "@radix-ui/react-select": "^2.1.0",