Skip to content

Commit

Permalink
Improve types for query (#36)
Browse files Browse the repository at this point in the history
Co-authored-by: Cal Irvine <[email protected]>
Co-authored-by: Ben Vinegar <[email protected]>
  • Loading branch information
3 people authored Feb 5, 2024
1 parent 7f36338 commit 71b54e3
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 87 deletions.
180 changes: 101 additions & 79 deletions app/analytics/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ColumnMappings } from "./schema";
import { ColumnMappingToType, ColumnMappings } from "./schema";

import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
Expand All @@ -10,9 +10,9 @@ dayjs.extend(timezone);
export interface AnalyticsQueryResultRow {
[key: string]: any;
}
interface AnalyticsQueryResult {
interface AnalyticsQueryResult<SelectionSet extends AnalyticsQueryResultRow> {
meta: string;
data: AnalyticsQueryResultRow[];
data: SelectionSet[];
rows: number;
rows_before_limit_at_least: number;
}
Expand Down Expand Up @@ -61,8 +61,8 @@ function formatDateString(d: Date) {
function generateEmptyRowsOverInterval(
intervalType: string,
daysAgo: number,
tz?: string,
): [Date, any] {
tz?: string
): [Date, { [key: string]: number }] {
if (!tz) {
tz = "Etc/UTC";
}
Expand All @@ -89,14 +89,14 @@ function generateEmptyRowsOverInterval(

const startDateTime = localDateTime.toDate();

const initialRows: any = {};
const initialRows: { [key: string]: number } = {};

for (let i = startDateTime.getTime(); i < Date.now(); i += intervalMs) {
// get date as utc
const rowDate = new Date(i);
// convert to UTC
const utcDateTime = new Date(
rowDate.getTime() + rowDate.getTimezoneOffset() * 60_000,
rowDate.getTime() + rowDate.getTimezoneOffset() * 60_000
);

const key = formatDateString(utcDateTime);
Expand Down Expand Up @@ -139,7 +139,7 @@ export class AnalyticsEngineAPI {
};
}

async query(query: string): Promise<Response> {
async query(query: string) {
return fetch(this.defaultUrl, {
method: "POST",
body: query,
Expand All @@ -151,8 +151,8 @@ export class AnalyticsEngineAPI {
siteId: string,
intervalType: string,
sinceDays: number,
tz: string,
): Promise<any> {
tz?: string
) {
let intervalCount = 1;

// keeping this code here once we start allowing bigger intervals (e.g. intervals of 2 hours)
Expand All @@ -167,7 +167,7 @@ export class AnalyticsEngineAPI {
const [startDateTime, initialRows] = generateEmptyRowsOverInterval(
intervalType,
sinceDays,
tz,
tz
);

// NOTE: when using toStartOfInterval, cannot group by other columns
Expand All @@ -189,49 +189,53 @@ export class AnalyticsEngineAPI {
GROUP BY _bucket
ORDER BY _bucket ASC`;

const queryResult = this.query(query);
const returnPromise = new Promise<any>((resolve, reject) =>
(async () => {
const response = await queryResult;
type SelectionSet = {
count: number;
_bucket: string;
bucket: string;
};

if (!response.ok) {
reject(response.statusText);
}
const queryResult = this.query(query);
const returnPromise = new Promise<[string, number][]>(
(resolve, reject) =>
(async () => {
const response = await queryResult;

const responseData =
(await response.json()) as AnalyticsQueryResult;

// note this query will return sparse data (i.e. only rows where count > 0)
// merge returnedRows with initial rows to fill in any gaps
const rowsByDateTime = responseData.data.reduce(
(accum, row) => {
const utcDateTime = new Date(row["bucket"]);
const key = formatDateString(utcDateTime);
accum[key] = row["count"];
return accum;
},
initialRows,
);
if (!response.ok) {
reject(response.statusText);
}

// return as sorted array of tuples (i.e. [datetime, count])
const sortedRows = Object.entries(rowsByDateTime).sort(
(a: any, b: any) => {
if (a[0] < b[0]) return -1;
else if (a[0] > b[0]) return 1;
else return 0;
},
);
const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

// note this query will return sparse data (i.e. only rows where count > 0)
// merge returnedRows with initial rows to fill in any gaps
const rowsByDateTime = responseData.data.reduce(
(accum, row) => {
const utcDateTime = new Date(row["bucket"]);
const key = formatDateString(utcDateTime);
accum[key] = row["count"];
return accum;
},
initialRows
);

// return as sorted array of tuples (i.e. [datetime, count])
const sortedRows = Object.entries(rowsByDateTime).sort(
(a, b) => {
if (a[0] < b[0]) return -1;
else if (a[0] > b[0]) return 1;
else return 0;
}
);

resolve(sortedRows);
})(),
resolve(sortedRows);
})()
);
return returnPromise;
}

async getCounts(
siteId: string,
sinceDays: number,
): Promise<AnalyticsCountResult> {
async getCounts(siteId: string, sinceDays: number) {
// defaults to 1 day if not specified
const interval = sinceDays || 1;
const siteIdColumn = ColumnMappings["siteId"];
Expand All @@ -246,6 +250,12 @@ export class AnalyticsEngineAPI {
GROUP BY isVisitor, isVisit
ORDER BY isVisitor, isVisit ASC`;

type SelectionSet = {
count: number;
isVisitor: number;
isVisit: number;
};

const queryResult = this.query(query);
const returnPromise = new Promise<AnalyticsCountResult>(
(resolve, reject) =>
Expand All @@ -257,7 +267,7 @@ export class AnalyticsEngineAPI {
}

const responseData =
(await response.json()) as AnalyticsQueryResult;
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

const counts: AnalyticsCountResult = {
views: 0,
Expand All @@ -277,23 +287,23 @@ export class AnalyticsEngineAPI {
counts.views += Number(row.count);
});
resolve(counts);
})(),
})()
);

return returnPromise;
}

async getVisitorCountByColumn(
async getVisitorCountByColumn<T extends keyof typeof ColumnMappings>(
siteId: string,
column: string,
column: T,
sinceDays: number,
limit?: number,
): Promise<any> {
limit?: number
) {
// defaults to 1 day if not specified
const interval = sinceDays || 1;
limit = limit || 10;

const _column: string = ColumnMappings[column];
const _column = ColumnMappings[column];
const query = `
SELECT ${_column}, SUM(_sample_interval) as count
FROM metricsDataset
Expand All @@ -304,8 +314,17 @@ export class AnalyticsEngineAPI {
ORDER BY count DESC
LIMIT ${limit}`;

type SelectionSet = {
count: number;
} & Record<
(typeof ColumnMappings)[T],
ColumnMappingToType<(typeof ColumnMappings)[T]>
>;

const queryResult = this.query(query);
const returnPromise = new Promise<any>((resolve, reject) =>
const returnPromise = new Promise<
[ColumnMappingToType<typeof _column> | "(none)", number][]
>((resolve, reject) =>
(async () => {
const response = await queryResult;

Expand All @@ -314,20 +333,20 @@ export class AnalyticsEngineAPI {
}

const responseData =
(await response.json()) as AnalyticsQueryResult;
(await response.json()) as AnalyticsQueryResult<SelectionSet>;
resolve(
responseData.data.map((row) => {
const key =
row[_column] === "" ? "(none)" : row[_column];
return [key, row["count"]];
}),
return [key, row["count"]] as const;
})
);
})(),
})()
);
return returnPromise;
}

async getCountByUserAgent(siteId: string, sinceDays: number): Promise<any> {
async getCountByUserAgent(siteId: string, sinceDays: number) {
return this.getVisitorCountByColumn(siteId, "userAgent", sinceDays);
}

Expand All @@ -351,10 +370,7 @@ export class AnalyticsEngineAPI {
return this.getVisitorCountByColumn(siteId, "deviceModel", sinceDays);
}

async getSitesOrderedByHits(
sinceDays: number,
limit?: number,
): Promise<any> {
async getSitesOrderedByHits(sinceDays: number, limit?: number) {
// defaults to 1 day if not specified
const interval = sinceDays || 1;
limit = limit || 10;
Expand All @@ -369,25 +385,31 @@ export class AnalyticsEngineAPI {
LIMIT ${limit}
`;

type SelectionSet = {
count: number;
siteId: string;
};

const queryResult = this.query(query);
const returnPromise = new Promise<any>((resolve, reject) =>
(async () => {
const response = await queryResult;
const returnPromise = new Promise<[string, number][]>(
(resolve, reject) =>
(async () => {
const response = await queryResult;

if (!response.ok) {
reject(response.statusText);
return;
}
if (!response.ok) {
reject(response.statusText);
return;
}

const responseData =
(await response.json()) as AnalyticsQueryResult;
const result = responseData.data.reduce((acc, cur) => {
acc.push([cur["siteId"], cur["count"]]);
return acc;
}, []);

resolve(result);
})(),
const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;
const result = responseData.data.reduce((acc, cur) => {
acc.push([cur["siteId"], cur["count"]]);
return acc;
}, [] as [string, number][]);

resolve(result);
})()
);
return returnPromise;
}
Expand Down
14 changes: 9 additions & 5 deletions app/analytics/schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
export interface ColumnMappingsType {
[key: string]: string;
}
export type ColumnMappingToType<
T extends (typeof ColumnMappings)[keyof typeof ColumnMappings]
> = T extends `blob${number}`
? string
: T extends `double${number}`
? number
: never;

/**
* This maps logical column names to the actual column names in the data store.
*/
export const ColumnMappings: ColumnMappingsType = {
export const ColumnMappings = {
/**
* blobs
*/
Expand All @@ -27,4 +31,4 @@ export const ColumnMappings: ColumnMappingsType = {

// this record is a new session (resets after 30m inactivity)
newSession: "double2",
};
} as const;
6 changes: 3 additions & 3 deletions app/routes/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ declare module "@remix-run/server-runtime" {
export const loader = async ({ context, request }: LoaderFunctionArgs) => {
const analyticsEngine = new AnalyticsEngineAPI(
context.env.CF_ACCOUNT_ID,
context.env.CF_BEARER_TOKEN,
context.env.CF_BEARER_TOKEN
);

const url = new URL(request.url);
Expand Down Expand Up @@ -130,7 +130,7 @@ export const loader = async ({ context, request }: LoaderFunctionArgs) => {
};

function convertCountryCodesToNames(
countByCountry: AnalyticsQueryResultRow[],
countByCountry: AnalyticsQueryResultRow[]
): AnalyticsQueryResultRow[] {
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
return countByCountry.map((countByBrowserRow: AnalyticsQueryResultRow) => {
Expand Down Expand Up @@ -167,7 +167,7 @@ export default function Dashboard() {
});
}

const chartData: any = [];
const chartData: { date: string; views: number }[] = [];
data.viewsGroupedByInterval.forEach((row: AnalyticsQueryResultRow) => {
chartData.push({
date: row[0],
Expand Down

0 comments on commit 71b54e3

Please sign in to comment.