@wise-old-man/utils 3.3.11 → 3.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var axios = require('axios');
4
+ var prometheus = require('prom-client');
5
+ var zod = require('zod');
6
+ require('dotenv/config');
7
+ var fetchable = require('@attio/fetchable');
8
+
3
9
  var config = {
4
10
  defaultUserAgent: `WiseOldMan JS Client v${process.env.npm_package_version}`,
5
11
  baseAPIUrl: 'https://api.wiseoldman.net/v2'
@@ -644,6 +650,8 @@ const PlayerType = {
644
650
  };
645
651
  const PlayerAnnotationType = {
646
652
  OPT_OUT: 'opt_out',
653
+ OPT_OUT_GROUPS: 'opt_out_groups',
654
+ OPT_OUT_COMPETITIONS: 'opt_out_competitions',
647
655
  BLOCKED: 'blocked',
648
656
  FAKE_F2P: 'fake_f2p'
649
657
  };
@@ -2059,6 +2067,226 @@ function getParentEfficiencyMetric(metric) {
2059
2067
  return null;
2060
2068
  }
2061
2069
 
2070
+ /**
2071
+ * This file has been created as a way to force any usage
2072
+ * of process.env to go through a dotenv.config first.
2073
+ */
2074
+ /**
2075
+ * This ensures that an env var is required in prod but optional in dev/test.
2076
+ */
2077
+ function prodOnly(varSchema) {
2078
+ if (process.env.NODE_ENV === 'production') {
2079
+ return varSchema;
2080
+ }
2081
+ return zod.z.optional(varSchema);
2082
+ }
2083
+ const envVariablesSchema = zod.z.object({
2084
+ // Prisma Database URL
2085
+ CORE_DATABASE_URL: zod.z.string().trim().min(1),
2086
+ // Redis Configs
2087
+ REDIS_HOST: zod.z.string().trim().min(1),
2088
+ REDIS_PORT: zod.z.coerce.number().positive().int(),
2089
+ // Node Environment
2090
+ NODE_ENV: zod.z.enum(['development', 'production', 'test']),
2091
+ // Port for the API to run on
2092
+ API_PORT: zod.z.optional(zod.z.coerce.number().positive().int()),
2093
+ // Admin Password (For mod+ operations)
2094
+ ADMIN_PASSWORD: prodOnly(zod.z.string().trim().min(1)),
2095
+ // Sentry (for error tracking)
2096
+ API_SENTRY_DSN: prodOnly(zod.z.string().trim().min(1)),
2097
+ // Patreon Token (to access their API)
2098
+ PATREON_BEARER_TOKEN: prodOnly(zod.z.string().trim().min(1)),
2099
+ // Discord Bot API URL (to send events to)
2100
+ DISCORD_BOT_API_URL: prodOnly(zod.z.string().trim().min(1).url()),
2101
+ // Our Prometheus metrics aggregator service URL
2102
+ PROMETHEUS_METRICS_SERVICE_URL: prodOnly(zod.z.string().trim().min(1).url()),
2103
+ // Discord Monitoring Webhooks
2104
+ DISCORD_PATREON_WEBHOOK_URL: prodOnly(zod.z.string().trim().min(1).url()),
2105
+ DISCORD_MONITORING_WEBHOOK_URL: prodOnly(zod.z.string().trim().min(1).url()),
2106
+ // Proxy Configs
2107
+ PROXY_LIST: prodOnly(zod.z.string().trim().min(1)),
2108
+ PROXY_USER: prodOnly(zod.z.string().trim().min(1)),
2109
+ PROXY_PASSWORD: prodOnly(zod.z.string().trim().min(1)),
2110
+ PROXY_PORT: prodOnly(zod.z.coerce.number().positive().int()),
2111
+ CPU_COUNT: prodOnly(zod.z.coerce.number().positive().int()),
2112
+ // Openai API Key
2113
+ OPENAI_API_KEY: prodOnly(zod.z.string().trim().min(1).startsWith('sk-'))
2114
+ });
2115
+ // This will load env vars from a .env file, type check them,and throw an error
2116
+ // (interrupting the process) if they're required and missing, or of an invalid type.
2117
+ try {
2118
+ envVariablesSchema.parse(process.env);
2119
+ }
2120
+ catch (error) {
2121
+ const errorPayload = JSON.stringify(error, null, 2);
2122
+ throw new Error(`Invalid environment variables. Please check env.ts for more info.\n${errorPayload}`);
2123
+ }
2124
+ function getThreadIndex() {
2125
+ if (process.env.pm_id === undefined) {
2126
+ return null;
2127
+ }
2128
+ return parseInt(process.env.pm_id, 10);
2129
+ }
2130
+
2131
+ class PrometheusService {
2132
+ constructor() {
2133
+ this.pushInterval = null;
2134
+ this.registry = new prometheus.Registry();
2135
+ this.registry.setDefaultLabels({ app: 'wise-old-man', threadIndex: getThreadIndex() });
2136
+ prometheus.collectDefaultMetrics({ register: this.registry });
2137
+ this.effectHistogram = new prometheus.Histogram({
2138
+ name: 'effect_duration_seconds',
2139
+ help: 'Duration of effects in microseconds',
2140
+ labelNames: ['effectName', 'status'],
2141
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2142
+ });
2143
+ this.httpHistogram = new prometheus.Histogram({
2144
+ name: 'http_request_duration_seconds',
2145
+ help: 'Duration of HTTP requests in microseconds',
2146
+ labelNames: ['method', 'route', 'status', 'userAgent'],
2147
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2148
+ });
2149
+ this.jobHistogram = new prometheus.Histogram({
2150
+ name: 'job_duration_seconds',
2151
+ help: 'Duration of jobs in microseconds',
2152
+ labelNames: ['jobName', 'status'],
2153
+ buckets: [0.1, 0.5, 1, 5, 10, 30, 60]
2154
+ });
2155
+ this.jobQueueGauge = new prometheus.Gauge({
2156
+ name: 'job_queue_size',
2157
+ help: 'Number of jobs in different states for each queue',
2158
+ labelNames: ['queueName', 'state']
2159
+ });
2160
+ this.eventCounter = new prometheus.Counter({
2161
+ name: 'event_counter',
2162
+ help: 'Count of events emitted',
2163
+ labelNames: ['eventType']
2164
+ });
2165
+ this.customPeriodCounter = new prometheus.Counter({
2166
+ name: 'custom_period_counter',
2167
+ help: 'Count of custom period expressions used',
2168
+ labelNames: ['customPeriod']
2169
+ });
2170
+ this.updatePlayerJobSourceCounter = new prometheus.Counter({
2171
+ name: 'update_player_job_source_counter',
2172
+ help: 'Count of update player jobs dispatched',
2173
+ labelNames: ['source']
2174
+ });
2175
+ this.runeMetricsHistogram = new prometheus.Histogram({
2176
+ name: 'runemetrics_duration_seconds',
2177
+ help: 'Duration of RuneMetrics requests in microseconds',
2178
+ labelNames: ['status'],
2179
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2180
+ });
2181
+ this.hiscoresHistogram = new prometheus.Histogram({
2182
+ name: 'hiscores_duration_seconds',
2183
+ help: 'Duration of hiscores requests in microseconds',
2184
+ labelNames: ['status'],
2185
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2186
+ });
2187
+ this.registry.registerMetric(this.jobHistogram);
2188
+ this.registry.registerMetric(this.jobQueueGauge);
2189
+ this.registry.registerMetric(this.httpHistogram);
2190
+ this.registry.registerMetric(this.effectHistogram);
2191
+ this.registry.registerMetric(this.eventCounter);
2192
+ this.registry.registerMetric(this.customPeriodCounter);
2193
+ this.registry.registerMetric(this.updatePlayerJobSourceCounter);
2194
+ this.registry.registerMetric(this.runeMetricsHistogram);
2195
+ this.registry.registerMetric(this.hiscoresHistogram);
2196
+ }
2197
+ init() {
2198
+ this.pushInterval = setInterval(() => {
2199
+ this.pushMetrics();
2200
+ }, 60000);
2201
+ }
2202
+ shutdown() {
2203
+ if (this.pushInterval !== null) {
2204
+ clearInterval(this.pushInterval);
2205
+ }
2206
+ }
2207
+ pushMetrics() {
2208
+ return __awaiter(this, void 0, void 0, function* () {
2209
+ if (process.env.NODE_ENV === 'test') {
2210
+ return fetchable.errored({ code: 'NOT_ALLOWED_IN_TEST_ENV' });
2211
+ }
2212
+ if (!process.env.PROMETHEUS_METRICS_SERVICE_URL) {
2213
+ return fetchable.errored({ code: 'MISSING_METRICS_URL' });
2214
+ }
2215
+ const metricsResult = yield fetchable.fromPromise(this.registry.getMetricsAsJSON());
2216
+ if (fetchable.isErrored(metricsResult)) {
2217
+ return fetchable.errored({
2218
+ code: 'FAILED_TO_GET_PROMETHEUS_METRICS',
2219
+ subError: metricsResult.error
2220
+ });
2221
+ }
2222
+ const requestResult = yield fetchable.fromPromise(axios.post(process.env.PROMETHEUS_METRICS_SERVICE_URL, {
2223
+ source: 'api',
2224
+ data: metricsResult.value,
2225
+ threadIndex: getThreadIndex()
2226
+ }));
2227
+ if (fetchable.isErrored(requestResult)) {
2228
+ return fetchable.errored({
2229
+ code: 'FAILED_TO_PUSH_PROMETHEUS_METRICS',
2230
+ subError: requestResult.error
2231
+ });
2232
+ }
2233
+ return fetchable.complete(true);
2234
+ });
2235
+ }
2236
+ trackHttpRequest() {
2237
+ return this.httpHistogram.startTimer();
2238
+ }
2239
+ trackRuneMetricsRequest() {
2240
+ return this.runeMetricsHistogram.startTimer();
2241
+ }
2242
+ trackHiscoresRequest() {
2243
+ return this.hiscoresHistogram.startTimer();
2244
+ }
2245
+ trackEffect(effectName, fn) {
2246
+ return __awaiter(this, void 0, void 0, function* () {
2247
+ const endTimer = this.effectHistogram.startTimer();
2248
+ try {
2249
+ yield fn();
2250
+ endTimer({ effectName, status: 1 });
2251
+ }
2252
+ catch (error) {
2253
+ endTimer({ effectName, status: 0 });
2254
+ throw error;
2255
+ }
2256
+ });
2257
+ }
2258
+ trackJob(jobName, handler) {
2259
+ return __awaiter(this, void 0, void 0, function* () {
2260
+ const endTimer = this.jobHistogram.startTimer();
2261
+ try {
2262
+ yield handler();
2263
+ endTimer({ jobName, status: 1 });
2264
+ }
2265
+ catch (error) {
2266
+ endTimer({ jobName, status: 0 });
2267
+ throw error;
2268
+ }
2269
+ });
2270
+ }
2271
+ trackEventEmitted(eventType) {
2272
+ this.eventCounter.inc({ eventType });
2273
+ }
2274
+ trackCustomPeriodExpression(customPeriod) {
2275
+ this.customPeriodCounter.inc({ customPeriod });
2276
+ }
2277
+ trackUpdatePlayerJobSource(source) {
2278
+ this.updatePlayerJobSourceCounter.inc({ source });
2279
+ }
2280
+ updateQueueMetrics(queueName, counts) {
2281
+ return __awaiter(this, void 0, void 0, function* () {
2282
+ for (const [state, count] of Object.entries(counts)) {
2283
+ this.jobQueueGauge.set({ queueName, state }, count);
2284
+ }
2285
+ });
2286
+ }
2287
+ }
2288
+ var prometheusService = new PrometheusService();
2289
+
2062
2290
  const CUSTOM_PERIOD_REGEX = /(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?/;
2063
2291
  const PeriodProps = {
2064
2292
  [Period.FIVE_MIN]: { name: '5 Min', milliseconds: 300000 },
@@ -2086,6 +2314,7 @@ function parsePeriodExpression(periodExpression) {
2086
2314
  durationMs: PeriodProps[fixed].milliseconds
2087
2315
  };
2088
2316
  }
2317
+ prometheusService.trackCustomPeriodExpression(fixed);
2089
2318
  const result = fixed.match(CUSTOM_PERIOD_REGEX);
2090
2319
  if (!result || result.length === 0 || result[0] !== fixed)
2091
2320
  return null;
package/dist/es/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ import axios from 'axios';
2
+ import prometheus from 'prom-client';
3
+ import { z } from 'zod';
4
+ import 'dotenv/config';
5
+ import { errored, fromPromise, isErrored, complete } from '@attio/fetchable';
6
+
1
7
  var config = {
2
8
  defaultUserAgent: `WiseOldMan JS Client v${process.env.npm_package_version}`,
3
9
  baseAPIUrl: 'https://api.wiseoldman.net/v2'
@@ -642,6 +648,8 @@ const PlayerType = {
642
648
  };
643
649
  const PlayerAnnotationType = {
644
650
  OPT_OUT: 'opt_out',
651
+ OPT_OUT_GROUPS: 'opt_out_groups',
652
+ OPT_OUT_COMPETITIONS: 'opt_out_competitions',
645
653
  BLOCKED: 'blocked',
646
654
  FAKE_F2P: 'fake_f2p'
647
655
  };
@@ -2057,6 +2065,226 @@ function getParentEfficiencyMetric(metric) {
2057
2065
  return null;
2058
2066
  }
2059
2067
 
2068
+ /**
2069
+ * This file has been created as a way to force any usage
2070
+ * of process.env to go through a dotenv.config first.
2071
+ */
2072
+ /**
2073
+ * This ensures that an env var is required in prod but optional in dev/test.
2074
+ */
2075
+ function prodOnly(varSchema) {
2076
+ if (process.env.NODE_ENV === 'production') {
2077
+ return varSchema;
2078
+ }
2079
+ return z.optional(varSchema);
2080
+ }
2081
+ const envVariablesSchema = z.object({
2082
+ // Prisma Database URL
2083
+ CORE_DATABASE_URL: z.string().trim().min(1),
2084
+ // Redis Configs
2085
+ REDIS_HOST: z.string().trim().min(1),
2086
+ REDIS_PORT: z.coerce.number().positive().int(),
2087
+ // Node Environment
2088
+ NODE_ENV: z.enum(['development', 'production', 'test']),
2089
+ // Port for the API to run on
2090
+ API_PORT: z.optional(z.coerce.number().positive().int()),
2091
+ // Admin Password (For mod+ operations)
2092
+ ADMIN_PASSWORD: prodOnly(z.string().trim().min(1)),
2093
+ // Sentry (for error tracking)
2094
+ API_SENTRY_DSN: prodOnly(z.string().trim().min(1)),
2095
+ // Patreon Token (to access their API)
2096
+ PATREON_BEARER_TOKEN: prodOnly(z.string().trim().min(1)),
2097
+ // Discord Bot API URL (to send events to)
2098
+ DISCORD_BOT_API_URL: prodOnly(z.string().trim().min(1).url()),
2099
+ // Our Prometheus metrics aggregator service URL
2100
+ PROMETHEUS_METRICS_SERVICE_URL: prodOnly(z.string().trim().min(1).url()),
2101
+ // Discord Monitoring Webhooks
2102
+ DISCORD_PATREON_WEBHOOK_URL: prodOnly(z.string().trim().min(1).url()),
2103
+ DISCORD_MONITORING_WEBHOOK_URL: prodOnly(z.string().trim().min(1).url()),
2104
+ // Proxy Configs
2105
+ PROXY_LIST: prodOnly(z.string().trim().min(1)),
2106
+ PROXY_USER: prodOnly(z.string().trim().min(1)),
2107
+ PROXY_PASSWORD: prodOnly(z.string().trim().min(1)),
2108
+ PROXY_PORT: prodOnly(z.coerce.number().positive().int()),
2109
+ CPU_COUNT: prodOnly(z.coerce.number().positive().int()),
2110
+ // Openai API Key
2111
+ OPENAI_API_KEY: prodOnly(z.string().trim().min(1).startsWith('sk-'))
2112
+ });
2113
+ // This will load env vars from a .env file, type check them,and throw an error
2114
+ // (interrupting the process) if they're required and missing, or of an invalid type.
2115
+ try {
2116
+ envVariablesSchema.parse(process.env);
2117
+ }
2118
+ catch (error) {
2119
+ const errorPayload = JSON.stringify(error, null, 2);
2120
+ throw new Error(`Invalid environment variables. Please check env.ts for more info.\n${errorPayload}`);
2121
+ }
2122
+ function getThreadIndex() {
2123
+ if (process.env.pm_id === undefined) {
2124
+ return null;
2125
+ }
2126
+ return parseInt(process.env.pm_id, 10);
2127
+ }
2128
+
2129
+ class PrometheusService {
2130
+ constructor() {
2131
+ this.pushInterval = null;
2132
+ this.registry = new prometheus.Registry();
2133
+ this.registry.setDefaultLabels({ app: 'wise-old-man', threadIndex: getThreadIndex() });
2134
+ prometheus.collectDefaultMetrics({ register: this.registry });
2135
+ this.effectHistogram = new prometheus.Histogram({
2136
+ name: 'effect_duration_seconds',
2137
+ help: 'Duration of effects in microseconds',
2138
+ labelNames: ['effectName', 'status'],
2139
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2140
+ });
2141
+ this.httpHistogram = new prometheus.Histogram({
2142
+ name: 'http_request_duration_seconds',
2143
+ help: 'Duration of HTTP requests in microseconds',
2144
+ labelNames: ['method', 'route', 'status', 'userAgent'],
2145
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2146
+ });
2147
+ this.jobHistogram = new prometheus.Histogram({
2148
+ name: 'job_duration_seconds',
2149
+ help: 'Duration of jobs in microseconds',
2150
+ labelNames: ['jobName', 'status'],
2151
+ buckets: [0.1, 0.5, 1, 5, 10, 30, 60]
2152
+ });
2153
+ this.jobQueueGauge = new prometheus.Gauge({
2154
+ name: 'job_queue_size',
2155
+ help: 'Number of jobs in different states for each queue',
2156
+ labelNames: ['queueName', 'state']
2157
+ });
2158
+ this.eventCounter = new prometheus.Counter({
2159
+ name: 'event_counter',
2160
+ help: 'Count of events emitted',
2161
+ labelNames: ['eventType']
2162
+ });
2163
+ this.customPeriodCounter = new prometheus.Counter({
2164
+ name: 'custom_period_counter',
2165
+ help: 'Count of custom period expressions used',
2166
+ labelNames: ['customPeriod']
2167
+ });
2168
+ this.updatePlayerJobSourceCounter = new prometheus.Counter({
2169
+ name: 'update_player_job_source_counter',
2170
+ help: 'Count of update player jobs dispatched',
2171
+ labelNames: ['source']
2172
+ });
2173
+ this.runeMetricsHistogram = new prometheus.Histogram({
2174
+ name: 'runemetrics_duration_seconds',
2175
+ help: 'Duration of RuneMetrics requests in microseconds',
2176
+ labelNames: ['status'],
2177
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2178
+ });
2179
+ this.hiscoresHistogram = new prometheus.Histogram({
2180
+ name: 'hiscores_duration_seconds',
2181
+ help: 'Duration of hiscores requests in microseconds',
2182
+ labelNames: ['status'],
2183
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2184
+ });
2185
+ this.registry.registerMetric(this.jobHistogram);
2186
+ this.registry.registerMetric(this.jobQueueGauge);
2187
+ this.registry.registerMetric(this.httpHistogram);
2188
+ this.registry.registerMetric(this.effectHistogram);
2189
+ this.registry.registerMetric(this.eventCounter);
2190
+ this.registry.registerMetric(this.customPeriodCounter);
2191
+ this.registry.registerMetric(this.updatePlayerJobSourceCounter);
2192
+ this.registry.registerMetric(this.runeMetricsHistogram);
2193
+ this.registry.registerMetric(this.hiscoresHistogram);
2194
+ }
2195
+ init() {
2196
+ this.pushInterval = setInterval(() => {
2197
+ this.pushMetrics();
2198
+ }, 60000);
2199
+ }
2200
+ shutdown() {
2201
+ if (this.pushInterval !== null) {
2202
+ clearInterval(this.pushInterval);
2203
+ }
2204
+ }
2205
+ pushMetrics() {
2206
+ return __awaiter(this, void 0, void 0, function* () {
2207
+ if (process.env.NODE_ENV === 'test') {
2208
+ return errored({ code: 'NOT_ALLOWED_IN_TEST_ENV' });
2209
+ }
2210
+ if (!process.env.PROMETHEUS_METRICS_SERVICE_URL) {
2211
+ return errored({ code: 'MISSING_METRICS_URL' });
2212
+ }
2213
+ const metricsResult = yield fromPromise(this.registry.getMetricsAsJSON());
2214
+ if (isErrored(metricsResult)) {
2215
+ return errored({
2216
+ code: 'FAILED_TO_GET_PROMETHEUS_METRICS',
2217
+ subError: metricsResult.error
2218
+ });
2219
+ }
2220
+ const requestResult = yield fromPromise(axios.post(process.env.PROMETHEUS_METRICS_SERVICE_URL, {
2221
+ source: 'api',
2222
+ data: metricsResult.value,
2223
+ threadIndex: getThreadIndex()
2224
+ }));
2225
+ if (isErrored(requestResult)) {
2226
+ return errored({
2227
+ code: 'FAILED_TO_PUSH_PROMETHEUS_METRICS',
2228
+ subError: requestResult.error
2229
+ });
2230
+ }
2231
+ return complete(true);
2232
+ });
2233
+ }
2234
+ trackHttpRequest() {
2235
+ return this.httpHistogram.startTimer();
2236
+ }
2237
+ trackRuneMetricsRequest() {
2238
+ return this.runeMetricsHistogram.startTimer();
2239
+ }
2240
+ trackHiscoresRequest() {
2241
+ return this.hiscoresHistogram.startTimer();
2242
+ }
2243
+ trackEffect(effectName, fn) {
2244
+ return __awaiter(this, void 0, void 0, function* () {
2245
+ const endTimer = this.effectHistogram.startTimer();
2246
+ try {
2247
+ yield fn();
2248
+ endTimer({ effectName, status: 1 });
2249
+ }
2250
+ catch (error) {
2251
+ endTimer({ effectName, status: 0 });
2252
+ throw error;
2253
+ }
2254
+ });
2255
+ }
2256
+ trackJob(jobName, handler) {
2257
+ return __awaiter(this, void 0, void 0, function* () {
2258
+ const endTimer = this.jobHistogram.startTimer();
2259
+ try {
2260
+ yield handler();
2261
+ endTimer({ jobName, status: 1 });
2262
+ }
2263
+ catch (error) {
2264
+ endTimer({ jobName, status: 0 });
2265
+ throw error;
2266
+ }
2267
+ });
2268
+ }
2269
+ trackEventEmitted(eventType) {
2270
+ this.eventCounter.inc({ eventType });
2271
+ }
2272
+ trackCustomPeriodExpression(customPeriod) {
2273
+ this.customPeriodCounter.inc({ customPeriod });
2274
+ }
2275
+ trackUpdatePlayerJobSource(source) {
2276
+ this.updatePlayerJobSourceCounter.inc({ source });
2277
+ }
2278
+ updateQueueMetrics(queueName, counts) {
2279
+ return __awaiter(this, void 0, void 0, function* () {
2280
+ for (const [state, count] of Object.entries(counts)) {
2281
+ this.jobQueueGauge.set({ queueName, state }, count);
2282
+ }
2283
+ });
2284
+ }
2285
+ }
2286
+ var prometheusService = new PrometheusService();
2287
+
2060
2288
  const CUSTOM_PERIOD_REGEX = /(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?/;
2061
2289
  const PeriodProps = {
2062
2290
  [Period.FIVE_MIN]: { name: '5 Min', milliseconds: 300000 },
@@ -2084,6 +2312,7 @@ function parsePeriodExpression(periodExpression) {
2084
2312
  durationMs: PeriodProps[fixed].milliseconds
2085
2313
  };
2086
2314
  }
2315
+ prometheusService.trackCustomPeriodExpression(fixed);
2087
2316
  const result = fixed.match(CUSTOM_PERIOD_REGEX);
2088
2317
  if (!result || result.length === 0 || result[0] !== fixed)
2089
2318
  return null;
package/dist/es/index.mjs CHANGED
@@ -1,3 +1,9 @@
1
+ import axios from 'axios';
2
+ import prometheus from 'prom-client';
3
+ import { z } from 'zod';
4
+ import 'dotenv/config';
5
+ import { errored, fromPromise, isErrored, complete } from '@attio/fetchable';
6
+
1
7
  var config = {
2
8
  defaultUserAgent: `WiseOldMan JS Client v${process.env.npm_package_version}`,
3
9
  baseAPIUrl: 'https://api.wiseoldman.net/v2'
@@ -642,6 +648,8 @@ const PlayerType = {
642
648
  };
643
649
  const PlayerAnnotationType = {
644
650
  OPT_OUT: 'opt_out',
651
+ OPT_OUT_GROUPS: 'opt_out_groups',
652
+ OPT_OUT_COMPETITIONS: 'opt_out_competitions',
645
653
  BLOCKED: 'blocked',
646
654
  FAKE_F2P: 'fake_f2p'
647
655
  };
@@ -2057,6 +2065,226 @@ function getParentEfficiencyMetric(metric) {
2057
2065
  return null;
2058
2066
  }
2059
2067
 
2068
+ /**
2069
+ * This file has been created as a way to force any usage
2070
+ * of process.env to go through a dotenv.config first.
2071
+ */
2072
+ /**
2073
+ * This ensures that an env var is required in prod but optional in dev/test.
2074
+ */
2075
+ function prodOnly(varSchema) {
2076
+ if (process.env.NODE_ENV === 'production') {
2077
+ return varSchema;
2078
+ }
2079
+ return z.optional(varSchema);
2080
+ }
2081
+ const envVariablesSchema = z.object({
2082
+ // Prisma Database URL
2083
+ CORE_DATABASE_URL: z.string().trim().min(1),
2084
+ // Redis Configs
2085
+ REDIS_HOST: z.string().trim().min(1),
2086
+ REDIS_PORT: z.coerce.number().positive().int(),
2087
+ // Node Environment
2088
+ NODE_ENV: z.enum(['development', 'production', 'test']),
2089
+ // Port for the API to run on
2090
+ API_PORT: z.optional(z.coerce.number().positive().int()),
2091
+ // Admin Password (For mod+ operations)
2092
+ ADMIN_PASSWORD: prodOnly(z.string().trim().min(1)),
2093
+ // Sentry (for error tracking)
2094
+ API_SENTRY_DSN: prodOnly(z.string().trim().min(1)),
2095
+ // Patreon Token (to access their API)
2096
+ PATREON_BEARER_TOKEN: prodOnly(z.string().trim().min(1)),
2097
+ // Discord Bot API URL (to send events to)
2098
+ DISCORD_BOT_API_URL: prodOnly(z.string().trim().min(1).url()),
2099
+ // Our Prometheus metrics aggregator service URL
2100
+ PROMETHEUS_METRICS_SERVICE_URL: prodOnly(z.string().trim().min(1).url()),
2101
+ // Discord Monitoring Webhooks
2102
+ DISCORD_PATREON_WEBHOOK_URL: prodOnly(z.string().trim().min(1).url()),
2103
+ DISCORD_MONITORING_WEBHOOK_URL: prodOnly(z.string().trim().min(1).url()),
2104
+ // Proxy Configs
2105
+ PROXY_LIST: prodOnly(z.string().trim().min(1)),
2106
+ PROXY_USER: prodOnly(z.string().trim().min(1)),
2107
+ PROXY_PASSWORD: prodOnly(z.string().trim().min(1)),
2108
+ PROXY_PORT: prodOnly(z.coerce.number().positive().int()),
2109
+ CPU_COUNT: prodOnly(z.coerce.number().positive().int()),
2110
+ // Openai API Key
2111
+ OPENAI_API_KEY: prodOnly(z.string().trim().min(1).startsWith('sk-'))
2112
+ });
2113
+ // This will load env vars from a .env file, type check them,and throw an error
2114
+ // (interrupting the process) if they're required and missing, or of an invalid type.
2115
+ try {
2116
+ envVariablesSchema.parse(process.env);
2117
+ }
2118
+ catch (error) {
2119
+ const errorPayload = JSON.stringify(error, null, 2);
2120
+ throw new Error(`Invalid environment variables. Please check env.ts for more info.\n${errorPayload}`);
2121
+ }
2122
+ function getThreadIndex() {
2123
+ if (process.env.pm_id === undefined) {
2124
+ return null;
2125
+ }
2126
+ return parseInt(process.env.pm_id, 10);
2127
+ }
2128
+
2129
+ class PrometheusService {
2130
+ constructor() {
2131
+ this.pushInterval = null;
2132
+ this.registry = new prometheus.Registry();
2133
+ this.registry.setDefaultLabels({ app: 'wise-old-man', threadIndex: getThreadIndex() });
2134
+ prometheus.collectDefaultMetrics({ register: this.registry });
2135
+ this.effectHistogram = new prometheus.Histogram({
2136
+ name: 'effect_duration_seconds',
2137
+ help: 'Duration of effects in microseconds',
2138
+ labelNames: ['effectName', 'status'],
2139
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2140
+ });
2141
+ this.httpHistogram = new prometheus.Histogram({
2142
+ name: 'http_request_duration_seconds',
2143
+ help: 'Duration of HTTP requests in microseconds',
2144
+ labelNames: ['method', 'route', 'status', 'userAgent'],
2145
+ buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10, 30]
2146
+ });
2147
+ this.jobHistogram = new prometheus.Histogram({
2148
+ name: 'job_duration_seconds',
2149
+ help: 'Duration of jobs in microseconds',
2150
+ labelNames: ['jobName', 'status'],
2151
+ buckets: [0.1, 0.5, 1, 5, 10, 30, 60]
2152
+ });
2153
+ this.jobQueueGauge = new prometheus.Gauge({
2154
+ name: 'job_queue_size',
2155
+ help: 'Number of jobs in different states for each queue',
2156
+ labelNames: ['queueName', 'state']
2157
+ });
2158
+ this.eventCounter = new prometheus.Counter({
2159
+ name: 'event_counter',
2160
+ help: 'Count of events emitted',
2161
+ labelNames: ['eventType']
2162
+ });
2163
+ this.customPeriodCounter = new prometheus.Counter({
2164
+ name: 'custom_period_counter',
2165
+ help: 'Count of custom period expressions used',
2166
+ labelNames: ['customPeriod']
2167
+ });
2168
+ this.updatePlayerJobSourceCounter = new prometheus.Counter({
2169
+ name: 'update_player_job_source_counter',
2170
+ help: 'Count of update player jobs dispatched',
2171
+ labelNames: ['source']
2172
+ });
2173
+ this.runeMetricsHistogram = new prometheus.Histogram({
2174
+ name: 'runemetrics_duration_seconds',
2175
+ help: 'Duration of RuneMetrics requests in microseconds',
2176
+ labelNames: ['status'],
2177
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2178
+ });
2179
+ this.hiscoresHistogram = new prometheus.Histogram({
2180
+ name: 'hiscores_duration_seconds',
2181
+ help: 'Duration of hiscores requests in microseconds',
2182
+ labelNames: ['status'],
2183
+ buckets: [0.1, 0.3, 0.5, 1, 5, 10, 30]
2184
+ });
2185
+ this.registry.registerMetric(this.jobHistogram);
2186
+ this.registry.registerMetric(this.jobQueueGauge);
2187
+ this.registry.registerMetric(this.httpHistogram);
2188
+ this.registry.registerMetric(this.effectHistogram);
2189
+ this.registry.registerMetric(this.eventCounter);
2190
+ this.registry.registerMetric(this.customPeriodCounter);
2191
+ this.registry.registerMetric(this.updatePlayerJobSourceCounter);
2192
+ this.registry.registerMetric(this.runeMetricsHistogram);
2193
+ this.registry.registerMetric(this.hiscoresHistogram);
2194
+ }
2195
+ init() {
2196
+ this.pushInterval = setInterval(() => {
2197
+ this.pushMetrics();
2198
+ }, 60000);
2199
+ }
2200
+ shutdown() {
2201
+ if (this.pushInterval !== null) {
2202
+ clearInterval(this.pushInterval);
2203
+ }
2204
+ }
2205
+ pushMetrics() {
2206
+ return __awaiter(this, void 0, void 0, function* () {
2207
+ if (process.env.NODE_ENV === 'test') {
2208
+ return errored({ code: 'NOT_ALLOWED_IN_TEST_ENV' });
2209
+ }
2210
+ if (!process.env.PROMETHEUS_METRICS_SERVICE_URL) {
2211
+ return errored({ code: 'MISSING_METRICS_URL' });
2212
+ }
2213
+ const metricsResult = yield fromPromise(this.registry.getMetricsAsJSON());
2214
+ if (isErrored(metricsResult)) {
2215
+ return errored({
2216
+ code: 'FAILED_TO_GET_PROMETHEUS_METRICS',
2217
+ subError: metricsResult.error
2218
+ });
2219
+ }
2220
+ const requestResult = yield fromPromise(axios.post(process.env.PROMETHEUS_METRICS_SERVICE_URL, {
2221
+ source: 'api',
2222
+ data: metricsResult.value,
2223
+ threadIndex: getThreadIndex()
2224
+ }));
2225
+ if (isErrored(requestResult)) {
2226
+ return errored({
2227
+ code: 'FAILED_TO_PUSH_PROMETHEUS_METRICS',
2228
+ subError: requestResult.error
2229
+ });
2230
+ }
2231
+ return complete(true);
2232
+ });
2233
+ }
2234
+ trackHttpRequest() {
2235
+ return this.httpHistogram.startTimer();
2236
+ }
2237
+ trackRuneMetricsRequest() {
2238
+ return this.runeMetricsHistogram.startTimer();
2239
+ }
2240
+ trackHiscoresRequest() {
2241
+ return this.hiscoresHistogram.startTimer();
2242
+ }
2243
+ trackEffect(effectName, fn) {
2244
+ return __awaiter(this, void 0, void 0, function* () {
2245
+ const endTimer = this.effectHistogram.startTimer();
2246
+ try {
2247
+ yield fn();
2248
+ endTimer({ effectName, status: 1 });
2249
+ }
2250
+ catch (error) {
2251
+ endTimer({ effectName, status: 0 });
2252
+ throw error;
2253
+ }
2254
+ });
2255
+ }
2256
+ trackJob(jobName, handler) {
2257
+ return __awaiter(this, void 0, void 0, function* () {
2258
+ const endTimer = this.jobHistogram.startTimer();
2259
+ try {
2260
+ yield handler();
2261
+ endTimer({ jobName, status: 1 });
2262
+ }
2263
+ catch (error) {
2264
+ endTimer({ jobName, status: 0 });
2265
+ throw error;
2266
+ }
2267
+ });
2268
+ }
2269
+ trackEventEmitted(eventType) {
2270
+ this.eventCounter.inc({ eventType });
2271
+ }
2272
+ trackCustomPeriodExpression(customPeriod) {
2273
+ this.customPeriodCounter.inc({ customPeriod });
2274
+ }
2275
+ trackUpdatePlayerJobSource(source) {
2276
+ this.updatePlayerJobSourceCounter.inc({ source });
2277
+ }
2278
+ updateQueueMetrics(queueName, counts) {
2279
+ return __awaiter(this, void 0, void 0, function* () {
2280
+ for (const [state, count] of Object.entries(counts)) {
2281
+ this.jobQueueGauge.set({ queueName, state }, count);
2282
+ }
2283
+ });
2284
+ }
2285
+ }
2286
+ var prometheusService = new PrometheusService();
2287
+
2060
2288
  const CUSTOM_PERIOD_REGEX = /(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?/;
2061
2289
  const PeriodProps = {
2062
2290
  [Period.FIVE_MIN]: { name: '5 Min', milliseconds: 300000 },
@@ -2084,6 +2312,7 @@ function parsePeriodExpression(periodExpression) {
2084
2312
  durationMs: PeriodProps[fixed].milliseconds
2085
2313
  };
2086
2314
  }
2315
+ prometheusService.trackCustomPeriodExpression(fixed);
2087
2316
  const result = fixed.match(CUSTOM_PERIOD_REGEX);
2088
2317
  if (!result || result.length === 0 || result[0] !== fixed)
2089
2318
  return null;
package/dist/index.d.ts CHANGED
@@ -642,6 +642,8 @@ declare const PlayerType: {
642
642
  type PlayerType = (typeof PlayerType)[keyof typeof PlayerType];
643
643
  declare const PlayerAnnotationType: {
644
644
  readonly OPT_OUT: "opt_out";
645
+ readonly OPT_OUT_GROUPS: "opt_out_groups";
646
+ readonly OPT_OUT_COMPETITIONS: "opt_out_competitions";
645
647
  readonly BLOCKED: "blocked";
646
648
  readonly FAKE_F2P: "fake_f2p";
647
649
  };
@@ -1404,9 +1406,6 @@ interface GroupStatistics {
1404
1406
  averageStats: FormattedSnapshot;
1405
1407
  metricLeaders: MetricLeaders;
1406
1408
  }
1407
- type MemberRoleChangeEvent = Omit<MemberActivity, 'createdAt'>;
1408
- type MemberJoinedEvent = Omit<MemberActivity, 'createdAt' | 'previousRole'>;
1409
- type MemberLeftEvent = Omit<MemberActivity, 'createdAt' | 'previousRole'>;
1410
1409
  type MemberActivityWithPlayer = MemberActivity & {
1411
1410
  player: Player;
1412
1411
  };
@@ -2284,4 +2283,4 @@ declare class WOMClient extends BaseAPIClient {
2284
2283
  constructor(options?: WOMClientOptions);
2285
2284
  }
2286
2285
 
2287
- export { ACTIVITIES, type Achievement, type AchievementDefinition, type AchievementProgress, type AchievementTemplate, Activity, type ActivityDelta, ActivityType, type ActivityValue, type AssertPlayerTypeResponse, BOSSES, type Bonus, Boss, type BossDelta, type BossMetaConfig, type BossValue, CAPPED_MAX_TOTAL_XP, COMBAT_SKILLS, COMPETITION_STATUSES, COMPETITION_TYPES, COMPUTED_METRICS, COUNTRY_CODES, type ChangeMemberRolePayload, CompetitionCSVTableType, type CompetitionDetails, type CompetitionDetailsCSVParams, type CompetitionListItem, CompetitionStatus, CompetitionStatusProps, CompetitionType, CompetitionTypeProps, type CompetitionWithParticipations, type CompetitionsSearchFilter, ComputedMetric, type ComputedMetricDelta, type ComputedMetricValue, Country, type CountryDetails, CountryProps, type CreateCompetitionPayload, type CreateCompetitionResponse, type CreateGroupPayload, type CreateGroupResponse, type DeltaGroupLeaderboardEntry, type DeltaLeaderboardEntry, type DeltaLeaderboardFilter, type DenyContext, type EditCompetitionPayload, type EditGroupPayload, EfficiencyAlgorithmType, type EfficiencyAlgorithmTypeUnion, type EfficiencyLeaderboardsFilter, type ExtendedAchievement, type ExtendedAchievementWithPlayer, F2P_BOSSES, type FlaggedPlayerReviewContext, type FormattedSnapshot, GROUP_ROLES, type GenericCountMessageResponse, type GenericMessageResponse, type GetGroupGainsFilter, type GetPlayerGainsResponse, type Group, type GroupDetails, type GroupHiscoresActivityItem, type GroupHiscoresBossItem, type GroupHiscoresComputedMetricItem, type GroupHiscoresEntry, type GroupHiscoresSkillItem, type GroupListItem, type GroupMemberFragment, type GroupRecordsFilter, GroupRole, GroupRoleProps, type GroupStatistics, MAX_LEVEL, MAX_SKILL_EXP, MAX_VIRTUAL_LEVEL, MEMBER_SKILLS, METRICS, type MapOf, type MeasuredDeltaProgress, type MemberActivityWithPlayer, type MemberInput, type MemberJoinedEvent, type MemberLeftEvent, type MemberRoleChangeEvent, type MembershipWithGroup, type MembershipWithPlayer, Metric, type MetricLeaders, MetricMeasure, MetricProps, MetricType, type MetricValueKey, type NameChange, type NameChangeDetails, NameChangeStatus, type NameChangeWithPlayer, type NameChangesSearchFilter, PERIODS, PLAYER_BUILDS, PLAYER_STATUSES, PLAYER_TYPES, PRIVELEGED_GROUP_ROLES, type ParticipationWithCompetition, type ParticipationWithCompetitionAndStandings, type ParticipationWithPlayer, type ParticipationWithPlayerAndProgress, Period, PeriodProps, type Player, PlayerAnnotationType, type PlayerArchiveWithPlayer, PlayerBuild, PlayerBuildProps, type PlayerCompetitionStandingsFilter, type PlayerCompetitionsFilter, type PlayerDeltasMap, type PlayerDetails, type PlayerRecordsFilter, PlayerStatus, PlayerStatusProps, PlayerType, PlayerTypeProps, REAL_METRICS, REAL_SKILLS, type Record, type RecordLeaderboardEntry, type RecordLeaderboardFilter, SKILLS, SKILL_EXP_AT_99, Skill, type SkillDelta, type SkillMetaConfig, type SkillMetaMethod, type SkillValue, type SkipContext, type Snapshot, type SnapshotFragment, type Team, type TimeRangeFilter, type Top5ProgressResult, WOMClient, findCountry, findCountryByCode, findCountryByName, findGroupRole, findMetric, findPeriod, findPlayerBuild, findPlayerType, formatNumber, getCombatLevel, getExpForLevel, getLevel, getMetricMeasure, getMetricName, getMetricRankKey, getMetricValueKey, getMinimumValue, getParentEfficiencyMetric, isActivity, isBoss, isCompetitionStatus, isCompetitionType, isComputedMetric, isCountry, isGroupRole, isMetric, isPeriod, isPlayerBuild, isPlayerStatus, isPlayerType, isSkill, padNumber, parsePeriodExpression, round };
2286
+ export { ACTIVITIES, type Achievement, type AchievementDefinition, type AchievementProgress, type AchievementTemplate, Activity, type ActivityDelta, ActivityType, type ActivityValue, type AssertPlayerTypeResponse, BOSSES, type Bonus, Boss, type BossDelta, type BossMetaConfig, type BossValue, CAPPED_MAX_TOTAL_XP, COMBAT_SKILLS, COMPETITION_STATUSES, COMPETITION_TYPES, COMPUTED_METRICS, COUNTRY_CODES, type ChangeMemberRolePayload, CompetitionCSVTableType, type CompetitionDetails, type CompetitionDetailsCSVParams, type CompetitionListItem, CompetitionStatus, CompetitionStatusProps, CompetitionType, CompetitionTypeProps, type CompetitionWithParticipations, type CompetitionsSearchFilter, ComputedMetric, type ComputedMetricDelta, type ComputedMetricValue, Country, type CountryDetails, CountryProps, type CreateCompetitionPayload, type CreateCompetitionResponse, type CreateGroupPayload, type CreateGroupResponse, type DeltaGroupLeaderboardEntry, type DeltaLeaderboardEntry, type DeltaLeaderboardFilter, type DenyContext, type EditCompetitionPayload, type EditGroupPayload, EfficiencyAlgorithmType, type EfficiencyAlgorithmTypeUnion, type EfficiencyLeaderboardsFilter, type ExtendedAchievement, type ExtendedAchievementWithPlayer, F2P_BOSSES, type FlaggedPlayerReviewContext, type FormattedSnapshot, GROUP_ROLES, type GenericCountMessageResponse, type GenericMessageResponse, type GetGroupGainsFilter, type GetPlayerGainsResponse, type Group, type GroupDetails, type GroupHiscoresActivityItem, type GroupHiscoresBossItem, type GroupHiscoresComputedMetricItem, type GroupHiscoresEntry, type GroupHiscoresSkillItem, type GroupListItem, type GroupMemberFragment, type GroupRecordsFilter, GroupRole, GroupRoleProps, type GroupStatistics, MAX_LEVEL, MAX_SKILL_EXP, MAX_VIRTUAL_LEVEL, MEMBER_SKILLS, METRICS, type MapOf, type MeasuredDeltaProgress, type MemberActivityWithPlayer, type MemberInput, type MembershipWithGroup, type MembershipWithPlayer, Metric, type MetricLeaders, MetricMeasure, MetricProps, MetricType, type MetricValueKey, type NameChange, type NameChangeDetails, NameChangeStatus, type NameChangeWithPlayer, type NameChangesSearchFilter, PERIODS, PLAYER_BUILDS, PLAYER_STATUSES, PLAYER_TYPES, PRIVELEGED_GROUP_ROLES, type ParticipationWithCompetition, type ParticipationWithCompetitionAndStandings, type ParticipationWithPlayer, type ParticipationWithPlayerAndProgress, Period, PeriodProps, type Player, PlayerAnnotationType, type PlayerArchiveWithPlayer, PlayerBuild, PlayerBuildProps, type PlayerCompetitionStandingsFilter, type PlayerCompetitionsFilter, type PlayerDeltasMap, type PlayerDetails, type PlayerRecordsFilter, PlayerStatus, PlayerStatusProps, PlayerType, PlayerTypeProps, REAL_METRICS, REAL_SKILLS, type Record, type RecordLeaderboardEntry, type RecordLeaderboardFilter, SKILLS, SKILL_EXP_AT_99, Skill, type SkillDelta, type SkillMetaConfig, type SkillMetaMethod, type SkillValue, type SkipContext, type Snapshot, type SnapshotFragment, type Team, type TimeRangeFilter, type Top5ProgressResult, WOMClient, findCountry, findCountryByCode, findCountryByName, findGroupRole, findMetric, findPeriod, findPlayerBuild, findPlayerType, formatNumber, getCombatLevel, getExpForLevel, getLevel, getMetricMeasure, getMetricName, getMetricRankKey, getMetricValueKey, getMinimumValue, getParentEfficiencyMetric, isActivity, isBoss, isCompetitionStatus, isCompetitionType, isComputedMetric, isCountry, isGroupRole, isMetric, isPeriod, isPlayerBuild, isPlayerStatus, isPlayerType, isSkill, padNumber, parsePeriodExpression, round };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wise-old-man/utils",
3
- "version": "3.3.11",
3
+ "version": "3.3.12",
4
4
  "description": "A JavaScript/TypeScript client that interfaces and consumes the Wise Old Man API, an API that tracks and measures players' progress in Old School Runescape.",
5
5
  "keywords": [
6
6
  "wiseoldman",