diff --git a/app/common/Telemetry.ts b/app/common/Telemetry.ts index dc9f3085..cde716ec 100644 --- a/app/common/Telemetry.ts +++ b/app/common/Telemetry.ts @@ -986,6 +986,11 @@ export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) { `but received a value of type ${typeof value}` ); } + if (typeof value === 'string' && !hasTimezone(value)) { + throw new Error( + `Telemetry metadata ${key} of event ${event} has an ambiguous date string` + ); + } } else if (dataType !== typeof value) { throw new Error( `Telemetry metadata ${key} of event ${event} expected a value of type ${dataType} ` + @@ -996,4 +1001,11 @@ export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) { }; } +// Check that datetime looks like it has a timezone in it. If not, +// that could be a problem for whatever ingests the data. +function hasTimezone(isoDateString: string) { + // Use a regular expression to check for a timezone offset or 'Z' + return /([+-]\d{2}:\d{2}|Z)$/.test(isoDateString); +} + export type TelemetryEventChecker = (event: TelemetryEvent, metadata?: TelemetryMetadata) => void; diff --git a/app/gen-server/lib/Housekeeper.ts b/app/gen-server/lib/Housekeeper.ts index 8534c5a1..5784976b 100644 --- a/app/gen-server/lib/Housekeeper.ts +++ b/app/gen-server/lib/Housekeeper.ts @@ -13,6 +13,7 @@ import log from 'app/server/lib/log'; import { IPermitStore } from 'app/server/lib/Permit'; import { optStringParam, stringParam } from 'app/server/lib/requestUtils'; import * as express from 'express'; +import moment from 'moment'; import fetch from 'node-fetch'; import * as Fetch from 'node-fetch'; import { EntityManager } from 'typeorm'; @@ -185,8 +186,8 @@ export class Housekeeper { numDocs: Number(summary.num_docs), numWorkspaces: Number(summary.num_workspaces), numMembers: Number(summary.num_members), - lastActivity: summary.last_activity, - earliestDocCreatedAt: summary.earliest_doc_created_at, + lastActivity: normalizedDateTimeString(summary.last_activity), + earliestDocCreatedAt: normalizedDateTimeString(summary.earliest_doc_created_at), }, full: { stripePlanId: summary.stripe_plan_id, @@ -398,3 +399,29 @@ export class Housekeeper { }); } } + +/** + * Output an ISO8601 format datetime string, with timezone. + * Any string fed in without timezone is expected to be in UTC. + * + * When connected to postgres, dates will be extracted as Date objects, + * with timezone information. The normalization done here is not + * really needed in this case. + * + * Timestamps in SQLite are stored as UTC, and read as strings + * (without timezone information). The normalization here is + * pretty important in this case. + */ +function normalizedDateTimeString(dateTime: any): string { + if (!dateTime) { return dateTime; } + if (dateTime instanceof Date) { + return moment(dateTime).toISOString(); + } + if (typeof dateTime === 'string') { + // When SQLite returns a string, it will be in UTC. + // Need to make sure it actually have timezone info in it + // (will not by default). + return moment.utc(dateTime).toISOString(); + } + throw new Error(`normalizedDateTimeString cannot handle ${dateTime}`); +}