normalize logging of dates in siteUsage across databases (#675)

Date handling for SQLite and PostgreSQL is inconsistent. This makes
sure that dates in `siteUsage` logs are in a consistent format that
includes time zone information.
This commit is contained in:
Paul Fitzpatrick 2023-09-13 14:44:04 -04:00 committed by GitHub
parent e92e42c063
commit bc6f550471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 41 additions and 2 deletions

View File

@ -986,6 +986,11 @@ export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) {
`but received a value of type ${typeof value}` `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) { } else if (dataType !== typeof value) {
throw new Error( throw new Error(
`Telemetry metadata ${key} of event ${event} expected a value of type ${dataType} ` + `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; export type TelemetryEventChecker = (event: TelemetryEvent, metadata?: TelemetryMetadata) => void;

View File

@ -13,6 +13,7 @@ import log from 'app/server/lib/log';
import { IPermitStore } from 'app/server/lib/Permit'; import { IPermitStore } from 'app/server/lib/Permit';
import { optStringParam, stringParam } from 'app/server/lib/requestUtils'; import { optStringParam, stringParam } from 'app/server/lib/requestUtils';
import * as express from 'express'; import * as express from 'express';
import moment from 'moment';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import * as Fetch from 'node-fetch'; import * as Fetch from 'node-fetch';
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
@ -185,8 +186,8 @@ export class Housekeeper {
numDocs: Number(summary.num_docs), numDocs: Number(summary.num_docs),
numWorkspaces: Number(summary.num_workspaces), numWorkspaces: Number(summary.num_workspaces),
numMembers: Number(summary.num_members), numMembers: Number(summary.num_members),
lastActivity: summary.last_activity, lastActivity: normalizedDateTimeString(summary.last_activity),
earliestDocCreatedAt: summary.earliest_doc_created_at, earliestDocCreatedAt: normalizedDateTimeString(summary.earliest_doc_created_at),
}, },
full: { full: {
stripePlanId: summary.stripe_plan_id, 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}`);
}