add sentry
All checks were successful
deploy / deploy (push) Successful in 33s

This commit is contained in:
Lee 2023-10-31 14:22:06 +00:00
parent e0363de4ad
commit 4c6a8c80fb
7 changed files with 293 additions and 100 deletions

152
package-lock.json generated

@ -10,10 +10,12 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@influxdata/influxdb-client": "^1.33.2", "@influxdata/influxdb-client": "^1.33.2",
"@sentry/node": "^7.77.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"mongoose": "^7.6.3", "mongoose": "^7.6.3",
"node-cron": "^3.0.2",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"websocket": "^1.0.34" "websocket": "^1.0.34"
@ -21,6 +23,7 @@
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/node": "^20.8.9", "@types/node": "^20.8.9",
"@types/node-cron": "^3.0.10",
"@types/websocket": "^1.0.8" "@types/websocket": "^1.0.8"
} }
}, },
@ -38,6 +41,65 @@
"sparse-bitfield": "^3.0.3" "sparse-bitfield": "^3.0.3"
} }
}, },
"node_modules/@sentry-internal/tracing": {
"version": "7.77.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.77.0.tgz",
"integrity": "sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw==",
"dependencies": {
"@sentry/core": "7.77.0",
"@sentry/types": "7.77.0",
"@sentry/utils": "7.77.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/core": {
"version": "7.77.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.77.0.tgz",
"integrity": "sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==",
"dependencies": {
"@sentry/types": "7.77.0",
"@sentry/utils": "7.77.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/node": {
"version": "7.77.0",
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.77.0.tgz",
"integrity": "sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g==",
"dependencies": {
"@sentry-internal/tracing": "7.77.0",
"@sentry/core": "7.77.0",
"@sentry/types": "7.77.0",
"@sentry/utils": "7.77.0",
"https-proxy-agent": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/types": {
"version": "7.77.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.77.0.tgz",
"integrity": "sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/utils": {
"version": "7.77.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.77.0.tgz",
"integrity": "sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==",
"dependencies": {
"@sentry/types": "7.77.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.4", "version": "1.19.4",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz",
@ -101,6 +163,12 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/node-cron": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.10.tgz",
"integrity": "sha512-8thdLSpV7na8+bsmiyMH/KKQWOBg4WmoLInlGxWz7JJn8Mie+53QWaygSlc7f/AsMsCB5bBTVjoueAU6tRutTw==",
"dev": true
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.9", "version": "6.9.9",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz",
@ -174,6 +242,38 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@ -795,6 +895,39 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -1087,6 +1220,17 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
}, },
"node_modules/node-cron": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz",
"integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==",
"dependencies": {
"uuid": "8.3.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.6.1", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz",
@ -1568,6 +1712,14 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

@ -12,10 +12,12 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@influxdata/influxdb-client": "^1.33.2", "@influxdata/influxdb-client": "^1.33.2",
"@sentry/node": "^7.77.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"mongoose": "^7.6.3", "mongoose": "^7.6.3",
"node-cron": "^3.0.2",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"websocket": "^1.0.34" "websocket": "^1.0.34"
@ -23,6 +25,7 @@
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/node": "^20.8.9", "@types/node": "^20.8.9",
"@types/node-cron": "^3.0.10",
"@types/websocket": "^1.0.8" "@types/websocket": "^1.0.8"
} }
} }

47
src/api/api.ts Normal file

@ -0,0 +1,47 @@
import * as Sentry from "@sentry/node";
import express from "express";
import analyticsRoute from "./routes/analytics";
const app = express();
const port = process.env.PORT || 3000;
Sentry.init({
dsn: "https://19a8f6e661f41ed26ec9c221954594cf@sentry.fascinated.cc/3",
integrations: [
// enable HTTP calls tracing
new Sentry.Integrations.Http({ tracing: true }),
// enable Express.js middleware tracing
new Sentry.Integrations.Express({ app }),
new Sentry.Integrations.OnUncaughtException(),
new Sentry.Integrations.OnUnhandledRejection(),
new Sentry.Integrations.Mongo({
useMongoose: true,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0,
});
// Routes
analyticsRoute(app);
// The request handler must be the first middleware on the app
app.use(Sentry.Handlers.requestHandler());
// TracingHandler creates a trace for every incoming request
app.use(Sentry.Handlers.tracingHandler());
// The error handler must be registered before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());
// Optional fallthrough error handler
app.use(function onError(err: any, req: any, res: any, next: any) {
// The error id is attached to `res.sentry` to be returned
// and optionally displayed to the user for support.
res.statusCode = 500;
res.end(res.sentry + "\n");
});
app.listen(port, () => {
console.log(`API Server is running on http://localhost:${port}`);
});

@ -0,0 +1,85 @@
import { INFLUXDB_BUCKET, InfluxQueryAPI } from "../..";
import { formatString } from "../../utils/stringUtils";
import { parseTimeToMilliseconds } from "../../utils/timeUtils";
// Query to get the player count history for tge last 24 hours in 1 hour intervals
const getPlayerHistoryQuery = `from(bucket: "${INFLUXDB_BUCKET}")
|> range(start: -{})
|> filter(fn: (r) => r["_measurement"] == "scoresaber")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r["type"] == "player_count")
|> aggregateWindow(every: {}, fn: mean)
|> yield()
`;
const getScoreCountHistoryQuery = `from(bucket: "${INFLUXDB_BUCKET}")
|> range(start: -{})
|> filter(fn: (r) => r["_measurement"] == "scoresaber")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r["type"] == "score_count")
|> aggregateWindow(every: {}, fn: spread, createEmpty: true)
`;
export default function analyticsRoute(app: any) {
app.get("/analytics", async (req: any, res: any) => {
const before = new Date().getTime();
const timeQuery = req.query.time || "30d";
const timeInMs = parseTimeToMilliseconds(timeQuery.toString());
if (timeInMs > 30 * 24 * 60 * 60 * 1000) {
return res.status(400).json({
error: "Time range too large. Max time range is 30 days.",
});
}
const shouldUseLongerIntervals = timeInMs > 24 * 60 * 60 * 1000 * 7; // 7 days
const getActivePlayersHistory = async () => {
const rows = await InfluxQueryAPI.collectRows(
formatString(
getPlayerHistoryQuery,
false,
timeQuery,
shouldUseLongerIntervals ? "1d" : "1h"
)
);
let history = rows.map((row: any) => ({
time: row._time,
value: row._value !== null ? row._value.toFixed(0) : null,
}));
return history.sort(
(a: any, b: any) =>
new Date(a.time).getTime() - new Date(b.time).getTime()
);
};
const getScoreCountHistory = async () => {
const rows = await InfluxQueryAPI.collectRows(
formatString(
getScoreCountHistoryQuery,
false,
timeQuery,
shouldUseLongerIntervals ? "1d" : "1h"
)
);
let history = rows.map((row: any) => ({
time: row._time,
value: row._value !== null ? row._value.toFixed(0) : null,
}));
return history.sort(
(a: any, b: any) =>
new Date(a.time).getTime() - new Date(b.time).getTime()
);
};
const [activePlayersHistory, scoreCountHistory] = await Promise.all([
getActivePlayersHistory(),
getScoreCountHistory(),
]);
return res.json({
serverTimeTaken: new Date().getTime() - before + "ms",
activePlayersHistory,
scoreCountHistory,
});
});
}

@ -23,4 +23,4 @@ export const InfluxWriteAPI = influxClient.getWriteApi(
export const InfluxQueryAPI = influxClient.getQueryApi(INFLUXDB_ORG); export const InfluxQueryAPI = influxClient.getQueryApi(INFLUXDB_ORG);
require("./services/updateData"); require("./services/updateData");
require("./services/api"); require("./api/api");

@ -1,95 +0,0 @@
import express from "express";
import { INFLUXDB_BUCKET, InfluxQueryAPI } from "../index";
import { formatString } from "../utils/stringUtils";
import { parseTimeToMilliseconds } from "../utils/timeUtils";
const app = express();
const port = process.env.PORT || 3000;
// Query to get the player count history for tge last 24 hours in 1 hour intervals
const getPlayerHistoryQuery = `from(bucket: "${INFLUXDB_BUCKET}")
|> range(start: -{})
|> filter(fn: (r) => r["_measurement"] == "scoresaber")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r["type"] == "player_count")
|> aggregateWindow(every: {}, fn: mean)
|> yield()
`;
const getScoreCountHistoryQuery = `from(bucket: "${INFLUXDB_BUCKET}")
|> range(start: -{})
|> filter(fn: (r) => r["_measurement"] == "scoresaber")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r["type"] == "score_count")
|> aggregateWindow(every: {}, fn: spread, createEmpty: true)
`;
app.get("/", (req, res) => {
res.send("Hello!");
});
app.get("/analytics", async (req, res) => {
const before = new Date().getTime();
const timeQuery = req.query.time || "30d";
const timeInMs = parseTimeToMilliseconds(timeQuery.toString());
if (timeInMs > 30 * 24 * 60 * 60 * 1000) {
return res.status(400).json({
error: "Time range too large. Max time range is 30 days.",
});
}
const shouldUseLongerIntervals = timeInMs > 24 * 60 * 60 * 1000 * 7; // 7 days
const getActivePlayersHistory = async () => {
const rows = await InfluxQueryAPI.collectRows(
formatString(
getPlayerHistoryQuery,
false,
timeQuery,
shouldUseLongerIntervals ? "1d" : "1h"
)
);
let history = rows.map((row: any) => ({
time: row._time,
value: row._value !== null ? row._value.toFixed(0) : null,
}));
return history.sort(
(a: any, b: any) =>
new Date(a.time).getTime() - new Date(b.time).getTime()
);
};
const getScoreCountHistory = async () => {
const rows = await InfluxQueryAPI.collectRows(
formatString(
getScoreCountHistoryQuery,
false,
timeQuery,
shouldUseLongerIntervals ? "1d" : "1h"
)
);
let history = rows.map((row: any) => ({
time: row._time,
value: row._value !== null ? row._value.toFixed(0) : null,
}));
return history.sort(
(a: any, b: any) =>
new Date(a.time).getTime() - new Date(b.time).getTime()
);
};
const [activePlayersHistory, scoreCountHistory] = await Promise.all([
getActivePlayersHistory(),
getScoreCountHistory(),
]);
return res.json({
serverTimeTaken: new Date().getTime() - before + "ms",
activePlayersHistory,
scoreCountHistory,
});
});
app.listen(port, () => {
console.log(`API Server is running on http://localhost:${port}`);
});

@ -1,5 +1,6 @@
import { Point } from "@influxdata/influxdb-client"; import { Point } from "@influxdata/influxdb-client";
import axios from "axios"; import axios from "axios";
import cron from "node-cron";
import { w3cwebsocket as WebsocketClient } from "websocket"; import { w3cwebsocket as WebsocketClient } from "websocket";
import { InfluxWriteAPI } from ".."; import { InfluxWriteAPI } from "..";
import { connectMongo } from "../db/mongo"; import { connectMongo } from "../db/mongo";
@ -18,6 +19,7 @@ async function update() {
.intField("value", parseInt(count)) .intField("value", parseInt(count))
.timestamp(new Date()); .timestamp(new Date());
InfluxWriteAPI.writePoint(point); InfluxWriteAPI.writePoint(point);
console.log(`Updated player count to ${count}`);
if (totalScores) { if (totalScores) {
InfluxWriteAPI.writePoint( InfluxWriteAPI.writePoint(
@ -26,9 +28,8 @@ async function update() {
.intField("value", totalScores) .intField("value", totalScores)
.timestamp(new Date()) .timestamp(new Date())
); );
console.log(`Updated score count to ${totalScores}`);
} }
console.log(`Updated player count to ${count}`);
} }
async function connectWebsocket() { async function connectWebsocket() {
@ -83,7 +84,7 @@ async function connectWebsocket() {
leaderboardPlayerInfo: player, leaderboardPlayerInfo: player,
pp, pp,
} = score; } = score;
const { maxScore, stars, id: leaderboardId } = leaderboard; const { maxScore, stars } = leaderboard;
const hmdName = Headsets[hmd] || "Unknown"; const hmdName = Headsets[hmd] || "Unknown";
const countryId = normalizedRegionName(player.country) || "Unknown"; const countryId = normalizedRegionName(player.country) || "Unknown";
@ -144,4 +145,4 @@ async function connectWebsocket() {
update(); update();
connectWebsocket(); connectWebsocket();
setInterval(update, 60_000); // 1 minute cron.schedule("*/5 * * * *", update);