@goodandready/dsh-usage-guard 0.1.7 → 0.1.10
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.
- package/README.md +45 -1
- package/README.ru.md +33 -0
- package/README.zh.md +10 -0
- package/lib/client.js +386 -44
- package/lib/index.js +127 -57
- package/lib/patch.js +23 -30
- package/lib/telemetry.js +111 -0
- package/lib/updater.js +266 -0
- package/lib/usage.js +89 -76
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
// dsh-usage-guard —
|
|
1
|
+
// dsh-usage-guard — Token usage counter sanitizer and session history protector.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// DSH core aggregates token usage into four buckets, two of which are accessed without fallbacks:
|
|
4
4
|
//
|
|
5
|
-
// uncachedInputTokens: usage.inputTokens, //
|
|
6
|
-
// outputTokens: usage.outputTokens, //
|
|
5
|
+
// uncachedInputTokens: usage.inputTokens, // unguarded
|
|
6
|
+
// outputTokens: usage.outputTokens, // unguarded
|
|
7
7
|
// cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
8
8
|
// cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// так:
|
|
13
|
-
//
|
|
14
|
-
// history unavailable for session "...": expected number, received NaN
|
|
15
|
-
//
|
|
16
|
-
// Не «счётчик соврал», а «история не открывается вовсе» — и остаётся такой
|
|
17
|
-
// навсегда, потому что выжимка считается проигрыванием журнала заново.
|
|
18
|
-
//
|
|
19
|
-
// Отсюда и лечение. Плагин не переписывает журналы на диске: он встаёт перед
|
|
20
|
-
// сложением и подставляет ноль вместо пропажи. Проигрывание идёт через ту же
|
|
21
|
-
// точку, поэтому уже испорченные сессии открываются снова — сами, без починки
|
|
22
|
-
// файлов.
|
|
23
|
-
//
|
|
24
|
-
// Ноль вместо пропущенного счётчика — заведомо неправда. Но выбор не между
|
|
25
|
-
// точным счётом и приблизительным, а между приблизительным и нечитаемой
|
|
26
|
-
// историей.
|
|
10
|
+
// If a provider returns a usage sample missing either of the first two, accumulation yields NaN.
|
|
11
|
+
// Subsequent schema validation then rejects the entire session history projection.
|
|
27
12
|
|
|
28
13
|
import z from '@deepseek-ai/schemastery'
|
|
29
14
|
|
|
30
15
|
import { patchRegistry } from './patch.js'
|
|
16
|
+
import { telemetry } from './telemetry.js'
|
|
17
|
+
import { registerPluginUpdater } from './updater.js'
|
|
31
18
|
import { complaint, damage, healed, usageOf } from './usage.js'
|
|
32
19
|
|
|
33
|
-
export const name = 'dsh-usage-guard'
|
|
20
|
+
export const name = '@goodandready/dsh-usage-guard'
|
|
21
|
+
export const NS = 'dsh-usage-guard'
|
|
34
22
|
export const inject = ['sessionProjections']
|
|
35
23
|
|
|
36
24
|
export const Config = z.object({
|
|
@@ -45,6 +33,10 @@ export const Config = z.object({
|
|
|
45
33
|
.default(true)
|
|
46
34
|
.description('Log a line naming the turn, the step and the raw sample whenever a damaged one arrives. '
|
|
47
35
|
+ 'Each turn, step and field set is reported once, not once per replay.'),
|
|
36
|
+
maxStepTokens: z
|
|
37
|
+
.natural()
|
|
38
|
+
.default(0)
|
|
39
|
+
.description('Maximum allowed token count for a single turn step. Spikes exceeding this ceiling are clamped to prevent integer overflow and runaway stats. Set to 0 to disable.'),
|
|
48
40
|
})
|
|
49
41
|
|
|
50
42
|
export function apply(ctx, config) {
|
|
@@ -52,33 +44,84 @@ export function apply(ctx, config) {
|
|
|
52
44
|
try {
|
|
53
45
|
live = Config(structuredClone(config ?? {})) ?? config
|
|
54
46
|
} catch (_) {
|
|
55
|
-
live = Config({}) ?? { repair: true, report: true }
|
|
47
|
+
live = Config({}) ?? { repair: true, report: true, maxStepTokens: 0 }
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
ctx.inject(['settings'], (sctx) => {
|
|
59
51
|
try {
|
|
60
|
-
const scope = sctx.settings
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
const scope = sctx.settings?.register?.(NS, Config, { base: config })
|
|
53
|
+
if (!scope) {
|
|
54
|
+
const msg = `[dsh-usage-guard] settings service register() unavailable; running with default config`
|
|
55
|
+
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
56
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof scope.get === 'function') {
|
|
61
|
+
try {
|
|
62
|
+
const fresh = scope.get()
|
|
63
|
+
if (fresh && typeof fresh === 'object') live = fresh
|
|
64
|
+
} catch (readErr) {
|
|
65
|
+
const msg = `[dsh-usage-guard] failed to read current settings: ${readErr?.message || readErr}; running with initial config`
|
|
66
|
+
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
67
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const msg = `[dsh-usage-guard] settings scope has no get() method; running with default config`
|
|
71
|
+
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
72
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof scope.watch === 'function') {
|
|
76
|
+
sctx.effect(() => scope.watch((next) => {
|
|
77
|
+
if (next && typeof next === 'object') live = next
|
|
78
|
+
}))
|
|
79
|
+
} else {
|
|
80
|
+
const msg = `[dsh-usage-guard] settings scope has no watch() method; running with default config without live update subscription`
|
|
81
|
+
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
82
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = `[dsh-usage-guard] failed to register settings subscription: ${err?.message || err}; running with default config`
|
|
86
|
+
if (typeof sctx.logger?.warn === 'function') sctx.logger.warn(msg)
|
|
87
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Register one-click updater and telemetry endpoints if webServer is available
|
|
92
|
+
ctx.inject(['webServer'], (wctx) => {
|
|
93
|
+
try {
|
|
94
|
+
wctx.effect(() => registerPluginUpdater(wctx, {
|
|
95
|
+
endpoint: '/api/dsh-usage-guard/update',
|
|
96
|
+
packageName: '@goodandready/dsh-usage-guard',
|
|
97
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
98
|
+
}), 'dsh-usage-guard: plugin updater route')
|
|
99
|
+
|
|
100
|
+
wctx.effect(() => wctx.webServer.register({
|
|
101
|
+
kind: 'exact',
|
|
102
|
+
path: '/api/dsh-usage-guard/telemetry',
|
|
103
|
+
handler: async (req, res) => {
|
|
104
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
105
|
+
res.writeHead(200, {
|
|
106
|
+
'content-type': 'application/json; charset=utf-8',
|
|
107
|
+
'cache-control': 'no-store',
|
|
108
|
+
})
|
|
109
|
+
res.end(req.method === 'HEAD' ? undefined : JSON.stringify(telemetry.getSnapshot()))
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
res.writeHead(405, { allow: 'GET, HEAD' })
|
|
113
|
+
res.end()
|
|
114
|
+
},
|
|
115
|
+
}), 'dsh-usage-guard: telemetry route')
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const msg = `[dsh-usage-guard] failed to register routes on webServer: ${err?.message || err}`
|
|
118
|
+
if (typeof wctx.logger?.warn === 'function') wctx.logger.warn(msg)
|
|
119
|
+
else if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(msg)
|
|
120
|
+
}
|
|
69
121
|
})
|
|
70
122
|
|
|
71
|
-
// О каждом сочетании сессии, хода, шага и набора полей жалуемся один раз:
|
|
72
|
-
// выжимка считается проигрыванием журнала заново, и без этого одна и та же
|
|
73
|
-
// порция въезжала бы в журнал при каждом открытии истории.
|
|
74
|
-
// Честное FIFO-вытеснение с ограничением в 1000 записей предотвращает утечку
|
|
75
|
-
// памяти при длительной работе без сброса всей накопленной дедупликации.
|
|
76
123
|
const told = new Set()
|
|
77
124
|
const MAX_TOLD = 1000
|
|
78
|
-
|
|
79
|
-
// Кэш санитизированных событий: каждое событие проходит через 10-15 проекций DSH.
|
|
80
|
-
// WeakMap позволяет обработать событие один раз, отдавая остальным проекциям
|
|
81
|
-
// готовый результат за O(1) без повторного парсинга и без утечек памяти.
|
|
82
125
|
const eventCache = new WeakMap()
|
|
83
126
|
|
|
84
127
|
const guard = (event) => {
|
|
@@ -91,31 +134,57 @@ export function apply(ctx, config) {
|
|
|
91
134
|
eventCache.set(event, event)
|
|
92
135
|
return event
|
|
93
136
|
}
|
|
94
|
-
const bad = damage(found.usage)
|
|
137
|
+
const bad = damage(found.usage, { maxStepTokens: live?.maxStepTokens })
|
|
95
138
|
if (bad.length === 0) {
|
|
96
139
|
eventCache.set(event, event)
|
|
97
140
|
return event
|
|
98
141
|
}
|
|
99
142
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
const sessionTag = event?.sessionId ?? event?.data?.sessionId ?? event?.data?.turn ?? 'global'
|
|
144
|
+
const seen = `${String(sessionTag)}/${String(found.turn)}/${String(found.step)}/${bad.join(',')}`
|
|
145
|
+
|
|
146
|
+
let clampedAny = false
|
|
147
|
+
let fixedTokensCount = 0
|
|
148
|
+
const maxLimit = Number(live?.maxStepTokens) > 0 ? Number(live?.maxStepTokens) : 0
|
|
149
|
+
for (const f of bad) {
|
|
150
|
+
const val = found.usage?.[f]
|
|
151
|
+
if (maxLimit > 0 && typeof val === 'number' && val > maxLimit) {
|
|
152
|
+
clampedAny = true
|
|
153
|
+
fixedTokensCount += (val - maxLimit)
|
|
154
|
+
} else {
|
|
155
|
+
fixedTokensCount += 1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!told.has(seen)) {
|
|
160
|
+
if (told.size >= MAX_TOLD) {
|
|
161
|
+
const oldest = told.values().next().value
|
|
162
|
+
told.delete(oldest)
|
|
163
|
+
}
|
|
164
|
+
told.add(seen)
|
|
165
|
+
|
|
166
|
+
telemetry.recordIncident({
|
|
167
|
+
turn: found.turn,
|
|
168
|
+
step: found.step,
|
|
169
|
+
badFields: bad,
|
|
170
|
+
fixedTokenCount: fixedTokensCount,
|
|
171
|
+
clamped: clampedAny,
|
|
172
|
+
provider: event?.provider ?? event?.data?.provider ?? 'unknown',
|
|
173
|
+
sample: found.usage,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (live && live.report !== false) {
|
|
177
|
+
const msg = `[dsh-usage-guard] ${complaint(found, bad, { maxStepTokens: live?.maxStepTokens })}`
|
|
178
|
+
if (typeof ctx.logger?.warn === 'function') {
|
|
179
|
+
ctx.logger.warn(msg)
|
|
180
|
+
} else {
|
|
110
181
|
// eslint-disable-next-line no-console
|
|
111
|
-
console.warn(
|
|
112
|
-
} catch {
|
|
113
|
-
// Ошибка вывода не должна ломать обработку событий
|
|
182
|
+
console.warn(msg)
|
|
114
183
|
}
|
|
115
184
|
}
|
|
116
185
|
}
|
|
117
186
|
|
|
118
|
-
const result = live && live.repair === false ? event : healed(event, bad)
|
|
187
|
+
const result = live && live.repair === false ? event : healed(event, bad, { maxStepTokens: live?.maxStepTokens })
|
|
119
188
|
eventCache.set(event, result)
|
|
120
189
|
return result
|
|
121
190
|
}
|
|
@@ -123,7 +192,8 @@ export function apply(ctx, config) {
|
|
|
123
192
|
ctx.inject(['sessionProjections'], (pctx) => {
|
|
124
193
|
pctx.effect(
|
|
125
194
|
() => patchRegistry(pctx.sessionProjections, guard),
|
|
126
|
-
'dsh-usage-guard:
|
|
195
|
+
'dsh-usage-guard: sanitize usage counter samples',
|
|
127
196
|
)
|
|
128
197
|
})
|
|
129
198
|
}
|
|
199
|
+
export { telemetry }
|
package/lib/patch.js
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
//
|
|
1
|
+
// How the patch intercepts before state reduction.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// регистрации.
|
|
3
|
+
// The session projection registry is a service with an exposed registrations map. Each entry
|
|
4
|
+
// holds a sanitized copy of what the owner registered, and calls `apply` on that copy.
|
|
5
|
+
// Therefore, we patch the definition's apply method, not the original object passed at registration.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// настоящего вызова не доезжает. Поэтому цепляемся за саму карту записей —
|
|
12
|
-
// обычный Map, один и тот же для всех, кто до него дотянулся.
|
|
7
|
+
// Load order is not guaranteed, and in practice core projections may register later than our plugin.
|
|
8
|
+
// Patching the service's `register` method is ineffective because callers go through an intermediary.
|
|
9
|
+
// Hence, we hook into the registrations map itself — a standard Map instance shared by all participants.
|
|
13
10
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// обезвреживатель для здорового события возвращает ровно то же событие — тем,
|
|
19
|
-
// кто расхода не касается, он обходится в одно сравнение.
|
|
11
|
+
// All projections are wrapped, not just a single one. Token usage is consumed by multiple systems:
|
|
12
|
+
// context pressure calculations, session token sums, and analytics. Each would fail on a missing
|
|
13
|
+
// counter. Wrapping all projections with a guard that returns the exact object reference for healthy
|
|
14
|
+
// events costs only a single reference comparison for events without usage.
|
|
20
15
|
|
|
21
|
-
/**
|
|
16
|
+
/** Marker on patched functions to prevent double wrapping. */
|
|
22
17
|
export const PATCHED = Symbol.for('dsh-usage-guard.patched')
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
|
-
*
|
|
20
|
+
* Wrap the apply method of a registry definition, preserving `this` context.
|
|
26
21
|
*
|
|
27
|
-
* @
|
|
22
|
+
* @param def - Projection definition entry.
|
|
23
|
+
* @param guard - Event sanitizer function.
|
|
24
|
+
* @returns Cleanup function restoring the original method.
|
|
28
25
|
*/
|
|
29
26
|
export function wrapApply(def, guard) {
|
|
30
27
|
if (!def || typeof def.apply !== 'function' || def.apply[PATCHED]) return () => {}
|
|
@@ -40,27 +37,23 @@ export function wrapApply(def, guard) {
|
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
43
|
-
*
|
|
40
|
+
* Patch the session projections registry.
|
|
44
41
|
*
|
|
45
|
-
* @param registry -
|
|
46
|
-
* @param guard -
|
|
47
|
-
*
|
|
48
|
-
* @returns функция, снимающая всё поставленное, в обратном порядке.
|
|
42
|
+
* @param registry - `sessionProjections` service instance.
|
|
43
|
+
* @param guard - Event sanitizer function; receives an event and returns either the same event or a repaired copy.
|
|
44
|
+
* @returns Teardown function that removes all installed hooks in reverse order.
|
|
49
45
|
*/
|
|
50
46
|
export function patchRegistry(registry, guard) {
|
|
51
47
|
const undo = []
|
|
52
48
|
const map = registry && typeof registry === 'object' ? registry.registrations : undefined
|
|
53
49
|
if (!map || typeof map.set !== 'function' || typeof map.values !== 'function') return () => {}
|
|
54
50
|
|
|
55
|
-
//
|
|
51
|
+
// Wrap entries already registered before this patch
|
|
56
52
|
for (const entry of map.values()) {
|
|
57
53
|
if (entry && entry.def) undo.push(wrapApply(entry.def, guard))
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
//
|
|
61
|
-
// Save the current map.set (may be the prototype method or a previous patched version).
|
|
62
|
-
// We restore via explicit assignment — relying on `delete` to restore the prototype method
|
|
63
|
-
// is fragile when the environment mutates Map.prototype.
|
|
56
|
+
// Intercept entries registered in the future when inserted into the map
|
|
64
57
|
const originalSet = map.set
|
|
65
58
|
if (!originalSet[PATCHED]) {
|
|
66
59
|
const patched = function (key, value) {
|
|
@@ -78,4 +71,4 @@ export function patchRegistry(registry, guard) {
|
|
|
78
71
|
for (const step of undo.reverse()) step()
|
|
79
72
|
undo.length = 0
|
|
80
73
|
}
|
|
81
|
-
}
|
|
74
|
+
}
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// lib/telemetry.js — In-memory telemetry accumulator for dsh-usage-guard
|
|
2
|
+
// Tracks rescued malformed usage samples, fixed NaN counters, and clamped token spikes.
|
|
3
|
+
|
|
4
|
+
class UsageGuardTelemetry {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.rescuedEvents = 0;
|
|
7
|
+
this.fixedTokens = 0;
|
|
8
|
+
this.clampedSpikes = 0;
|
|
9
|
+
this.lastIncident = null;
|
|
10
|
+
this.maxRecentIncidents = 20;
|
|
11
|
+
this.recentIncidents = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
recordIncident({ turn = 0, step = 0, badFields = [], fixedTokenCount = 0, clamped = false, provider = 'unknown', sample = null, kind = 'malformed-usage', target = '' } = {}) {
|
|
15
|
+
this.rescuedEvents += 1;
|
|
16
|
+
this.fixedTokens += Number(fixedTokenCount) || 0;
|
|
17
|
+
if (clamped) {
|
|
18
|
+
this.clampedSpikes += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const incident = {
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
kind: String(kind),
|
|
24
|
+
turn: turn ?? 0,
|
|
25
|
+
step: step ?? 0,
|
|
26
|
+
target: String(target || (Array.isArray(badFields) ? badFields.join(',') : '')),
|
|
27
|
+
fields: Array.isArray(badFields) ? badFields : [],
|
|
28
|
+
clamped: Boolean(clamped),
|
|
29
|
+
provider: typeof provider === 'string' ? provider : 'unknown',
|
|
30
|
+
sample: sample && typeof sample === 'object' ? JSON.stringify(sample).slice(0, 300) : null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.lastIncident = incident;
|
|
34
|
+
this.recentIncidents.unshift(incident);
|
|
35
|
+
if (this.recentIncidents.length > this.maxRecentIncidents) {
|
|
36
|
+
this.recentIncidents.pop();
|
|
37
|
+
}
|
|
38
|
+
return incident;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
recordRescue(tokens = 0) {
|
|
42
|
+
this.rescuedEvents += 1;
|
|
43
|
+
this.fixedTokens += Number(tokens) || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
recordClamped(tokens = 0) {
|
|
47
|
+
this.clampedSpikes += 1;
|
|
48
|
+
this.fixedTokens += Number(tokens) || 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getSnapshot() {
|
|
52
|
+
return {
|
|
53
|
+
rescuedEvents: this.rescuedEvents,
|
|
54
|
+
fixedTokens: this.fixedTokens,
|
|
55
|
+
clampedSpikes: this.clampedSpikes,
|
|
56
|
+
lastIncident: this.lastIncident ? { ...this.lastIncident } : null,
|
|
57
|
+
recentIncidents: this.recentIncidents.map((i) => ({ ...i })),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reset() {
|
|
62
|
+
this.rescuedEvents = 0;
|
|
63
|
+
this.fixedTokens = 0;
|
|
64
|
+
this.clampedSpikes = 0;
|
|
65
|
+
this.lastIncident = null;
|
|
66
|
+
this.recentIncidents = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const telemetry = new UsageGuardTelemetry();
|
|
71
|
+
|
|
72
|
+
export function recordIncident(options) {
|
|
73
|
+
return telemetry.recordIncident(options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function recordRescue(tokens) {
|
|
77
|
+
return telemetry.recordRescue(tokens);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function recordClamped(tokens) {
|
|
81
|
+
return telemetry.recordClamped(tokens);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getSnapshot() {
|
|
85
|
+
return telemetry.getSnapshot();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resetTelemetry() {
|
|
89
|
+
telemetry.reset();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registerTelemetryRoute(ctx, options = {}) {
|
|
93
|
+
const endpoint = options.endpoint || '/api/dsh-usage-guard/telemetry';
|
|
94
|
+
return ctx.webServer.register({
|
|
95
|
+
kind: 'exact',
|
|
96
|
+
path: endpoint,
|
|
97
|
+
handler: async (request, response) => {
|
|
98
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
99
|
+
response.writeHead(405, { allow: 'GET, HEAD' });
|
|
100
|
+
response.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const snapshot = telemetry.getSnapshot();
|
|
104
|
+
response.writeHead(200, {
|
|
105
|
+
'content-type': 'application/json; charset=utf-8',
|
|
106
|
+
'cache-control': 'no-store',
|
|
107
|
+
});
|
|
108
|
+
response.end(request.method === 'HEAD' ? undefined : JSON.stringify(snapshot));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|