prism-debugger 0.2.0
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/.env.example +9 -0
- package/README.md +128 -0
- package/bin/prism.js +72 -0
- package/package.json +35 -0
- package/public/app.js +1516 -0
- package/public/index.html +214 -0
- package/public/styles.css +1224 -0
- package/src/broker.js +425 -0
- package/src/config.js +55 -0
- package/src/index.js +100 -0
- package/src/logger.js +72 -0
- package/src/plugins/context-device-event-logger.plugin.js +152 -0
- package/src/plugins/index.js +21 -0
- package/src/plugins/plugin-manager.js +51 -0
- package/src/storage.js +102 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdir, appendFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const SAFE_SEGMENT_RE = /[^a-zA-Z0-9._-]/g;
|
|
5
|
+
const SYSTEM_EVENTS = new Set(['debugger.register', 'debugger.unregister']);
|
|
6
|
+
|
|
7
|
+
const sanitizeSegment = (value, fallback) => {
|
|
8
|
+
const normalized = String(value ?? '')
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(SAFE_SEGMENT_RE, '_')
|
|
11
|
+
.replace(/_+/g, '_')
|
|
12
|
+
.replace(/^_+|_+$/g, '');
|
|
13
|
+
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
return normalized.slice(0, 96);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const pickContextName = (message) => {
|
|
21
|
+
const payload = message?.payload;
|
|
22
|
+
if (!payload || typeof payload !== 'object') {
|
|
23
|
+
return 'general';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof payload.context === 'string') {
|
|
27
|
+
return payload.context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (payload.context && typeof payload.context === 'object') {
|
|
31
|
+
if (typeof payload.context.name === 'string') {
|
|
32
|
+
return payload.context.name;
|
|
33
|
+
}
|
|
34
|
+
if (typeof payload.context.id === 'string') {
|
|
35
|
+
return payload.context.id;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof payload.screen === 'string') {
|
|
40
|
+
return payload.screen;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof payload.module === 'string') {
|
|
44
|
+
return payload.module;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof message.category === 'string' && message.category.trim()) {
|
|
48
|
+
return message.category;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (SYSTEM_EVENTS.has(message.eventName)) {
|
|
52
|
+
return 'system';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'general';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pickAppName = ({ debuggerState, message }) => {
|
|
59
|
+
const app = debuggerState?.app;
|
|
60
|
+
if (app && typeof app === 'object') {
|
|
61
|
+
if (typeof app.bundleId === 'string' && app.bundleId.trim()) {
|
|
62
|
+
return app.bundleId;
|
|
63
|
+
}
|
|
64
|
+
if (typeof app.appName === 'string' && app.appName.trim()) {
|
|
65
|
+
return app.appName;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = message?.payload;
|
|
70
|
+
if (payload?.app && typeof payload.app === 'object') {
|
|
71
|
+
if (typeof payload.app.bundleId === 'string' && payload.app.bundleId.trim()) {
|
|
72
|
+
return payload.app.bundleId;
|
|
73
|
+
}
|
|
74
|
+
if (typeof payload.app.appName === 'string' && payload.app.appName.trim()) {
|
|
75
|
+
return payload.app.appName;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (debuggerState?.name && typeof debuggerState.name === 'string' && debuggerState.name.trim()) {
|
|
80
|
+
return debuggerState.name;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return 'unknown-app';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const pickDeviceName = ({ debuggerState, message }) => {
|
|
87
|
+
const app = debuggerState?.app;
|
|
88
|
+
if (app && typeof app === 'object') {
|
|
89
|
+
if (typeof app.deviceModel === 'string' && app.deviceModel.trim()) {
|
|
90
|
+
return app.deviceModel;
|
|
91
|
+
}
|
|
92
|
+
if (typeof app.deviceName === 'string' && app.deviceName.trim()) {
|
|
93
|
+
return app.deviceName;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const payload = message?.payload;
|
|
98
|
+
if (payload?.app && typeof payload.app === 'object') {
|
|
99
|
+
if (typeof payload.app.deviceModel === 'string' && payload.app.deviceModel.trim()) {
|
|
100
|
+
return payload.app.deviceModel;
|
|
101
|
+
}
|
|
102
|
+
if (typeof payload.app.deviceName === 'string' && payload.app.deviceName.trim()) {
|
|
103
|
+
return payload.app.deviceName;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 'unknown-device';
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const pickSessionId = ({ debuggerState, message }) => {
|
|
111
|
+
if (message?.sessionId && typeof message.sessionId === 'string' && message.sessionId.trim()) {
|
|
112
|
+
return message.sessionId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (debuggerState?.sessionId && typeof debuggerState.sessionId === 'string' && debuggerState.sessionId.trim()) {
|
|
116
|
+
return debuggerState.sessionId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return 'no-session';
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const createContextDeviceEventLoggerPlugin = ({ baseDir }) => {
|
|
123
|
+
return {
|
|
124
|
+
name: 'context-device-event-logger',
|
|
125
|
+
async onInit() {
|
|
126
|
+
await mkdir(baseDir, { recursive: true });
|
|
127
|
+
},
|
|
128
|
+
async onIosEventAccepted({ ts, message, debuggerState }) {
|
|
129
|
+
const appName = sanitizeSegment(pickAppName({ debuggerState, message }), 'unknown-app');
|
|
130
|
+
const device = sanitizeSegment(pickDeviceName({ debuggerState, message }), 'unknown-device');
|
|
131
|
+
const sessionId = sanitizeSegment(pickSessionId({ debuggerState, message }), 'no-session');
|
|
132
|
+
const context = sanitizeSegment(pickContextName(message), 'general');
|
|
133
|
+
|
|
134
|
+
const sessionDir = path.join(baseDir, appName, device, sessionId);
|
|
135
|
+
const targetFile = path.join(sessionDir, `${context}.log`);
|
|
136
|
+
|
|
137
|
+
await mkdir(sessionDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
const row = {
|
|
140
|
+
ts,
|
|
141
|
+
debuggerId: message.debuggerId,
|
|
142
|
+
sessionId: message.sessionId ?? null,
|
|
143
|
+
eventName: message.eventName,
|
|
144
|
+
level: message.level ?? 'info',
|
|
145
|
+
category: message.category ?? null,
|
|
146
|
+
payload: message.payload ?? {}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await appendFile(targetFile, `${JSON.stringify(row)}\n`, 'utf8');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContextDeviceEventLoggerPlugin } from './context-device-event-logger.plugin.js';
|
|
2
|
+
|
|
3
|
+
export const createPluginsFromConfig = (config) => {
|
|
4
|
+
const registry = {
|
|
5
|
+
'context-device-event-logger': () =>
|
|
6
|
+
createContextDeviceEventLoggerPlugin({
|
|
7
|
+
baseDir: config.contextEventLogDir
|
|
8
|
+
})
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const plugins = [];
|
|
12
|
+
for (const pluginId of config.enabledPlugins) {
|
|
13
|
+
const factory = registry[pluginId];
|
|
14
|
+
if (!factory) {
|
|
15
|
+
console.warn(`[plugins] Unknown plugin "${pluginId}" ignored`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
plugins.push(factory());
|
|
19
|
+
}
|
|
20
|
+
return plugins;
|
|
21
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const noop = () => {};
|
|
2
|
+
|
|
3
|
+
const withFallbackMethods = (plugin) => ({
|
|
4
|
+
onInit: plugin.onInit ?? noop,
|
|
5
|
+
onStop: plugin.onStop ?? noop,
|
|
6
|
+
onIosEventAccepted: plugin.onIosEventAccepted ?? noop,
|
|
7
|
+
onServerEventSent: plugin.onServerEventSent ?? noop,
|
|
8
|
+
...plugin
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export class PluginManager {
|
|
12
|
+
constructor({ plugins = [] } = {}) {
|
|
13
|
+
this.plugins = plugins
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map(withFallbackMethods);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
listNames() {
|
|
19
|
+
return this.plugins.map((item) => item.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async init(context) {
|
|
23
|
+
await Promise.all(
|
|
24
|
+
this.plugins.map((plugin) => this.callHook(plugin, 'onInit', context))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async stop(context) {
|
|
29
|
+
await Promise.all(
|
|
30
|
+
this.plugins.map((plugin) => this.callHook(plugin, 'onStop', context))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
emit(hookName, context) {
|
|
35
|
+
for (const plugin of this.plugins) {
|
|
36
|
+
this.callHook(plugin, hookName, context);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
callHook(plugin, hookName, context) {
|
|
41
|
+
const hook = plugin[hookName];
|
|
42
|
+
if (typeof hook !== 'function') {
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Promise.resolve(hook(context)).catch((error) => {
|
|
47
|
+
const name = plugin.name ?? '<anonymous-plugin>';
|
|
48
|
+
console.error(`[plugin:${name}] ${hookName} failed:`, error?.stack ?? error);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class RingBuffer {
|
|
2
|
+
constructor(capacity) {
|
|
3
|
+
this.capacity = capacity;
|
|
4
|
+
this.items = [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
push(item) {
|
|
8
|
+
if (this.items.length >= this.capacity) {
|
|
9
|
+
this.items.shift();
|
|
10
|
+
}
|
|
11
|
+
this.items.push(item);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
toArray() {
|
|
15
|
+
return [...this.items];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class InMemoryStorage {
|
|
20
|
+
constructor({ ringBufferSize }) {
|
|
21
|
+
this.debuggers = new Map();
|
|
22
|
+
this.messagesByDebugger = new Map();
|
|
23
|
+
this.perfpointsByDebugger = new Map();
|
|
24
|
+
this.perfPointSpecsByDebugger = new Map();
|
|
25
|
+
this.ringBufferSize = ringBufferSize;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
upsertDebugger(debuggerState) {
|
|
29
|
+
this.debuggers.set(debuggerState.debuggerId, { ...debuggerState });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setDebuggerOffline(debuggerId, nowIso) {
|
|
33
|
+
const item = this.debuggers.get(debuggerId);
|
|
34
|
+
if (!item) return;
|
|
35
|
+
this.debuggers.set(debuggerId, {
|
|
36
|
+
...item,
|
|
37
|
+
status: 'offline',
|
|
38
|
+
lastSeen: nowIso
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
removeDebugger(debuggerId) {
|
|
43
|
+
this.debuggers.delete(debuggerId);
|
|
44
|
+
this.messagesByDebugger.delete(debuggerId);
|
|
45
|
+
this.perfpointsByDebugger.delete(debuggerId);
|
|
46
|
+
this.perfPointSpecsByDebugger.delete(debuggerId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getDebugger(debuggerId) {
|
|
50
|
+
return this.debuggers.get(debuggerId) ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
listDebuggers() {
|
|
54
|
+
return [...this.debuggers.values()].sort((a, b) =>
|
|
55
|
+
a.createdAt < b.createdAt ? 1 : -1
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addMessage(debuggerId, message) {
|
|
60
|
+
if (!this.messagesByDebugger.has(debuggerId)) {
|
|
61
|
+
this.messagesByDebugger.set(debuggerId, new RingBuffer(this.ringBufferSize));
|
|
62
|
+
}
|
|
63
|
+
this.messagesByDebugger.get(debuggerId).push(message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getMessages(debuggerId) {
|
|
67
|
+
return this.messagesByDebugger.get(debuggerId)?.toArray() ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addPerfPoint(debuggerId, point) {
|
|
71
|
+
if (!this.perfpointsByDebugger.has(debuggerId)) {
|
|
72
|
+
this.perfpointsByDebugger.set(debuggerId, new RingBuffer(5000));
|
|
73
|
+
}
|
|
74
|
+
this.perfpointsByDebugger.get(debuggerId).push(point);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getPerfPoints(debuggerId) {
|
|
78
|
+
return this.perfpointsByDebugger.get(debuggerId)?.toArray() ?? [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setPerfPointSpecs(debuggerId, specs) {
|
|
82
|
+
this.perfPointSpecsByDebugger.set(debuggerId, specs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getPerfPointSpecs(debuggerId) {
|
|
86
|
+
return this.perfPointSpecsByDebugger.get(debuggerId) ?? [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
removeOfflineDebuggers() {
|
|
90
|
+
const removed = [];
|
|
91
|
+
for (const [id, dbg] of this.debuggers) {
|
|
92
|
+
if (dbg.status === 'offline') {
|
|
93
|
+
removed.push(id);
|
|
94
|
+
this.debuggers.delete(id);
|
|
95
|
+
this.messagesByDebugger.delete(id);
|
|
96
|
+
this.perfpointsByDebugger.delete(id);
|
|
97
|
+
this.perfPointSpecsByDebugger.delete(id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return removed;
|
|
101
|
+
}
|
|
102
|
+
}
|