Skip to content

Commit

Permalink
🔨 chore: simple telemetry (optional) (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Apr 6, 2022
1 parent ee3c655 commit 2d4ff81
Show file tree
Hide file tree
Showing 17 changed files with 824 additions and 534 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ NODE_ENV="production"

# Do not change this, unless you know what you're doing!
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?sslmode=prefer

# Errors get reported to Linear (Project manager). This is done so errors can be fixed faster.
TELEMETRY_ENABLED="true"
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
"@tsed/common": "^6.107.5",
"@tsed/core": "^6.107.5",
"@tsed/di": "^6.107.5",
"@tsed/exceptions": "^6.107.5",
"@tsed/exceptions": "^6.110.1",
"@tsed/json-mapper": "^6.107.5",
"@tsed/platform-exceptions": "^6.110.1",
"@tsed/platform-express": "^6.107.5",
"@tsed/schema": "^6.107.5",
"@tsed/socketio": "^6.107.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Socket } from "services/SocketService";
import { nanoid } from "nanoid";
import { validateSchema } from "lib/validateSchema";
import type { cad, Feature } from "@prisma/client";
import { getCADVersion } from "src/main";
import { getCADVersion } from "@snailycad/utils/version";

@Controller("/admin/manage/cad-settings")
export class ManageCitizensController {
Expand Down
17 changes: 1 addition & 16 deletions packages/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { $log } from "@tsed/logger";
import { PlatformExpress } from "@tsed/platform-express";
import { Server } from "./server";
import { resolve } from "node:path";
import { readFile } from "node:fs/promises";
import process from "node:process";
import { getCADVersion } from "@snailycad/utils/version";

async function bootstrap() {
try {
Expand All @@ -19,17 +17,4 @@ async function bootstrap() {
}
}

let versionCache: string;
export async function getCADVersion(): Promise<string | null> {
const packageJsonPath = resolve(process.cwd(), "package.json");
const packageJson = await readFile(packageJsonPath, "utf-8").catch(() => null);
if (!packageJson) return null;

const json = JSON.parse(packageJson);

versionCache ??= json.version;

return versionCache;
}

bootstrap();
2 changes: 1 addition & 1 deletion packages/api/src/middlewares/IsAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BadRequest, Forbidden, Unauthorized } from "@tsed/exceptions";
import { getSessionUser, userProperties } from "lib/auth/user";
import { prisma } from "lib/prisma";
import { updateMemberRolesLogin } from "lib/discord/auth";
import { getCADVersion } from "src/main";
import { getCADVersion } from "@snailycad/utils/version";
import { allPermissions } from "@snailycad/permissions";

const CAD_SELECT = (user?: Pick<User, "rank">) => ({
Expand Down
78 changes: 68 additions & 10 deletions packages/api/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import "@tsed/swagger";
import "@tsed/socketio";
import "@tsed/platform-express";
import { join } from "node:path";
import process from "node:process";
import { Configuration, Inject, PlatformApplication, Response } from "@tsed/common";
import {
Configuration,
Inject,
PlatformApplication,
PlatformContext,
Response,
ResponseErrorObject,
} from "@tsed/common";
import { Catch, ExceptionFilterMethods } from "@tsed/platform-exceptions";
import type { Exception } from "@tsed/exceptions";
import { json } from "express";
import compress from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import { IsEnabled } from "middlewares/IsEnabled";
import { sendErrorReport } from "@snailycad/telemetry";

const rootDir = __dirname;

Expand All @@ -29,6 +40,13 @@ const rootDir = __dirname;
},
],
},
middlewares: [
cookieParser(),
compress(),
json(),
cors({ origin: process.env.CORS_ORIGIN_URL ?? "http://localhost:3000", credentials: true }),
IsEnabled,
],
swagger: [{ path: "/api-docs", specVersion: "3.0.3" }],
socketIO: {
cors: {
Expand All @@ -45,15 +63,6 @@ export class Server {
settings!: Configuration;

public $beforeRoutesInit() {
this.app
.use(cookieParser())
.use(compress())
.use(json())
.use(
cors({ origin: process.env.CORS_ORIGIN_URL ?? "http://localhost:3000", credentials: true }),
)
.use(IsEnabled);

if (process.env.EXPERIMENTAL_SECURE_CONTEXT) {
const app = this.app.callback();
app.set("trust proxy", 1);
Expand All @@ -67,3 +76,52 @@ export class Server {
});
}
}

@Catch(Error)
export class ErrorFilter implements ExceptionFilterMethods {
catch(exception: Exception, ctx: PlatformContext) {
const { response, logger } = ctx;
const error = this.mapError(exception);
const headers = this.getHeaders(exception);

logger.error({
error,
catch: true,
});

sendErrorReport({
name: error.name,
message: error.message,
stack: `${JSON.stringify(error.errors, null, 4)} \n\n\n ${JSON.stringify(error, null, 4)}`,
});

response
.setHeaders(headers)
.status(error.status || 500)
.body(error);
}

mapError(error: any) {
return {
name: error.origin?.name || error.name,
message: error.message,
status: error.status || 500,
errors: this.getErrors(error),
};
}

protected getErrors(error: any) {
return [error, error.origin].filter(Boolean).reduce((errs, { errors }: ResponseErrorObject) => {
return [...errs, ...(errors || [])];
}, []);
}

protected getHeaders(error: any) {
return [error, error.origin].filter(Boolean).reduce((obj, { headers }: ResponseErrorObject) => {
return {
...obj,
...(headers || {}),
};
}, {});
}
}
3 changes: 3 additions & 0 deletions packages/telemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @snailycad/telemetry

TODO
31 changes: 31 additions & 0 deletions packages/telemetry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@snailycad/telemetry",
"version": "1.0.0-beta.79",
"main": "./dist/index.js",
"scripts": {
"copy-env": "node ../../scripts/copy-env.mjs --telemetry",
"build": "tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@snailycad/utils": "1.0.0-beta.79",
"axios": "^0.26.1"
},
"devDependencies": {
"@types/node": "^17.0.23",
"tslib": "^2.3.1",
"tsup": "^5.12.1",
"typescript": "^4.6.3"
},
"tsup": {
"entry": [
"src/**/*.ts"
],
"dts": true,
"bundle": false,
"platform": "node",
"target": "node16",
"silent": true,
"minify": true
}
}
17 changes: 17 additions & 0 deletions packages/telemetry/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface TelemetryCache {
cadVersion: string | null;
node: string | null;
yarn: string | null;
npm: string | null;
}

const cache: Partial<TelemetryCache> = {};

export function set(key: keyof TelemetryCache, value: string) {
cache[key] = value;
return value;
}

export function get(key: keyof TelemetryCache) {
return cache[key];
}
67 changes: 67 additions & 0 deletions packages/telemetry/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import process from "node:process";
import axios from "axios";
import os from "node:os";
import { execSync } from "node:child_process";
import { get, set } from "./cache";
import { getCADVersion } from "@snailycad/utils/version";

const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED === "true";
const REPORT_URL = "https://snailycad-telemetry.caspertheghost.workers.dev/";

interface ErrorReport {
name: string;
message: string;
stack?: string;
}

export async function sendErrorReport(errorReport: ErrorReport) {
if (!TELEMETRY_ENABLED) return;

const [yarn, node, npm, cadVersion] = await Promise.all([
getBinaryVersions("yarn"),
getBinaryVersions("node"),
getBinaryVersions("npm"),
getCADVersion(),
]);

const data = {
yarn,
npm,
node,
cadVersion,
platform: os.platform(),
os: os.platform(),
stack: `\`\`\`${errorReport.stack || null}\`\`\``,
message: `\`\`\`${errorReport.message || null}\`\`\``,
name: errorReport.name || "Unknown Error",
};

try {
await axios({
url: REPORT_URL,
method: "POST",
data: JSON.stringify(data),
});
} catch (e: any) {
if (process.env.NODE_ENV === "development") {
console.log(e.response);
console.log(e.response.data.errors);
}
}
}

async function getBinaryVersions(command: "yarn" | "node" | "npm") {
const cache = get(command);

if (cache) {
return cache;
}

try {
const out = execSync(`${command} -v`, { encoding: "utf-8" }).toString().trim();
set(command, out);
return out;
} catch {
return null;
}
}
13 changes: 13 additions & 0 deletions packages/telemetry/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "node",
"declaration": true,
"skipLibCheck": true,
"importHelpers": true,
"esModuleInterop": true
}
}
7 changes: 7 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"require": "./dist/typeguards.js",
"import": "./dist/typeguards.mjs"
},
"./version": {
"require": "./dist/version.js",
"import": "./dist/version.mjs"
},
"./package.json": "./package.json"
},
"files": [
Expand All @@ -29,6 +33,9 @@
],
"typeguards": [
"dist/typeguards.d.ts"
],
"version": [
"dist/version.d.ts"
]
}
},
Expand Down
16 changes: 16 additions & 0 deletions packages/utils/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { resolve } from "node:path";
import { readFile } from "node:fs/promises";
import process from "node:process";

let versionCache: string;
export async function getCADVersion(): Promise<string | null> {
const packageJsonPath = resolve(process.cwd(), "package.json");
const packageJson = await readFile(packageJsonPath, "utf-8").catch(() => null);
if (!packageJson) return null;

const json = JSON.parse(packageJson);

versionCache ??= json.version;

return versionCache;
}
6 changes: 6 additions & 0 deletions packages/utils/tests/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, test } from "vitest";
import { getCADVersion } from "../src/version";

test("Should return the CAD version", async () => {
expect(await getCADVersion()).toBeTypeOf("string");
});
3 changes: 2 additions & 1 deletion packages/utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"moduleResolution": "node",
"declaration": true,
"skipLibCheck": true,
"importHelpers": true
"importHelpers": true,
"esModuleInterop": true
}
}
Loading

0 comments on commit 2d4ff81

Please sign in to comment.