peekinside 0.0.0-stage → 1.2.1
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/LICENSE +21 -0
- package/README.md +45 -2
- package/bin/peekinside.js +8 -0
- package/index.d.ts +389 -0
- package/index.js +3 -0
- package/package.json +77 -4
- package/src/PeekInside.js +731 -0
- package/src/capture/body-capture.js +86 -0
- package/src/capture/body-decoder.js +188 -0
- package/src/capture/exchange-recorder.js +263 -0
- package/src/capture/http-message.js +138 -0
- package/src/capture/ignored-paths.js +49 -0
- package/src/capture/request-body-tap.js +40 -0
- package/src/capture/response-tap.js +83 -0
- package/src/cli.js +254 -0
- package/src/config.js +183 -0
- package/src/crypto.js +189 -0
- package/src/deployment.js +183 -0
- package/src/exceptions/exception-builder.js +339 -0
- package/src/exceptions/exception-capture.js +407 -0
- package/src/exceptions/exception-record.js +118 -0
- package/src/exceptions/fingerprint.js +60 -0
- package/src/exceptions/frame-classifier.js +114 -0
- package/src/exceptions/limits.js +95 -0
- package/src/exceptions/process-monitor.js +192 -0
- package/src/exceptions/rate-limiter.js +50 -0
- package/src/exceptions/request-context.js +337 -0
- package/src/exceptions/safe-json.js +186 -0
- package/src/exceptions/source-context.js +144 -0
- package/src/exceptions/source-maps.js +313 -0
- package/src/exceptions/stack-parser.js +179 -0
- package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
- package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
- package/src/hub/client.js +184 -0
- package/src/hub/entry.js +4 -0
- package/src/hub/exception-state.js +97 -0
- package/src/hub/ingest-payload.js +362 -0
- package/src/hub/process.js +175 -0
- package/src/hub/project-log.js +559 -0
- package/src/hub/segment-log.js +282 -0
- package/src/hub/server.js +561 -0
- package/src/hub/spool-ingester.js +110 -0
- package/src/hub/store.js +404 -0
- package/src/instrumentation/console.js +83 -0
- package/src/instrumentation/http.js +146 -0
- package/src/instrumentation/index.js +234 -0
- package/src/instrumentation/jobs.js +186 -0
- package/src/instrumentation/loggers.js +135 -0
- package/src/instrumentation/mongodb.js +221 -0
- package/src/instrumentation/mysql2.js +161 -0
- package/src/instrumentation/operation.js +70 -0
- package/src/instrumentation/patch.js +72 -0
- package/src/instrumentation/pg.js +218 -0
- package/src/instrumentation/prisma.js +181 -0
- package/src/integrations/express-errors.js +135 -0
- package/src/integrations/express.js +57 -0
- package/src/integrations/fastify.js +72 -0
- package/src/integrations/framework-adapter.js +53 -0
- package/src/integrations/http-server.js +32 -0
- package/src/integrations/koa.js +72 -0
- package/src/integrations/server-binder.js +120 -0
- package/src/jobs/job-context.js +40 -0
- package/src/jobs/job-run-record.js +104 -0
- package/src/jobs/job-runner.js +173 -0
- package/src/logs/log-capture.js +143 -0
- package/src/logs/log-record.js +74 -0
- package/src/project.js +76 -0
- package/src/record.js +181 -0
- package/src/release.js +157 -0
- package/src/runtime/loop-monitor.js +162 -0
- package/src/runtime/sampler.js +93 -0
- package/src/spool.js +195 -0
- package/src/timeline/caller.js +129 -0
- package/src/timeline/execution-hash.js +110 -0
- package/src/timeline/lru.js +50 -0
- package/src/timeline/masking.js +266 -0
- package/src/timeline/shape.js +77 -0
- package/src/timeline/sql.js +396 -0
- package/src/timeline/timeline.js +642 -0
- package/src/transport/batch.js +324 -0
- package/src/transport/cloud.js +363 -0
- package/src/transport/local.js +354 -0
- package/src/user.js +83 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which request paths are never recorded: browser and dev-server noise, plus the app's own
|
|
3
|
+
* `ignorePaths` rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prefixes of browser and dev-server noise that is never worth recording.
|
|
8
|
+
*
|
|
9
|
+
* @type {ReadonlyArray<string>}
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_SKIP_PATHS = Object.freeze(['/.well-known/', '/favicon.ico', '/__webpack_hmr', '/sockjs-node/', '/_next/static/', '/static/']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether a path must not be recorded.
|
|
15
|
+
*
|
|
16
|
+
* The rules are passed on every call rather than kept, because apps may change their
|
|
17
|
+
* `ignorePaths` option after PeekInside starts (attachTo() merges new options in).
|
|
18
|
+
*
|
|
19
|
+
* @param {string} pathname - Request path, without the query string
|
|
20
|
+
* @param {string|RegExp|Function|Array<string|RegExp|Function>} rules - String prefixes, regular
|
|
21
|
+
* expressions, or predicates receiving the pathname. Empty strings match nothing, and a
|
|
22
|
+
* predicate that throws counts as no match.
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
export function isIgnoredPath(pathname, rules) {
|
|
26
|
+
if (DEFAULT_SKIP_PATHS.some(prefix => pathname.startsWith(prefix))) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const list = Array.isArray(rules) ? rules : [rules];
|
|
31
|
+
return list.some(rule => {
|
|
32
|
+
if (typeof rule === 'string') {
|
|
33
|
+
return rule !== '' && pathname.startsWith(rule);
|
|
34
|
+
}
|
|
35
|
+
if (rule instanceof RegExp) {
|
|
36
|
+
// A global or sticky expression would otherwise resume from where its last match ended.
|
|
37
|
+
rule.lastIndex = 0;
|
|
38
|
+
return rule.test(pathname);
|
|
39
|
+
}
|
|
40
|
+
if (typeof rule === 'function') {
|
|
41
|
+
try {
|
|
42
|
+
return Boolean(rule(pathname));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captures a request body as the app reads it.
|
|
3
|
+
*
|
|
4
|
+
* Reading stays the app's business: adding a 'data' listener would switch the stream into
|
|
5
|
+
* flowing mode, so data could flow before the app's body parser is ready for it. Wrapping
|
|
6
|
+
* emit() instead shows PeekInside every chunk the app receives, whichever middleware reads it
|
|
7
|
+
* and whenever, so middleware order does not matter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BodyCapture } from './body-capture.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Observes the 'data' events of one request.
|
|
14
|
+
*/
|
|
15
|
+
export class RequestBodyTap {
|
|
16
|
+
/**
|
|
17
|
+
* Starts observing `req`.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('http').IncomingMessage} req
|
|
20
|
+
* @param {number} limit - Most bytes kept
|
|
21
|
+
*/
|
|
22
|
+
constructor(req, limit) {
|
|
23
|
+
/**
|
|
24
|
+
* Bytes the app has read so far
|
|
25
|
+
* @type {BodyCapture}
|
|
26
|
+
*/
|
|
27
|
+
this.body = new BodyCapture(limit);
|
|
28
|
+
|
|
29
|
+
if (typeof req.emit === 'function') {
|
|
30
|
+
const body = this.body;
|
|
31
|
+
const originalEmit = req.emit;
|
|
32
|
+
req.emit = function (event, chunk) {
|
|
33
|
+
if (event === 'data') {
|
|
34
|
+
body.push(chunk);
|
|
35
|
+
}
|
|
36
|
+
return originalEmit.apply(this, arguments);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captures a response as the app sends it: the body passed to write() and end(), and the headers
|
|
3
|
+
* passed directly to writeHead(), which res.getHeaders() does not report.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BodyCapture } from './body-capture.js';
|
|
7
|
+
import { mergeHeaders, normalizeHeaders } from './http-message.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wraps the write methods of one response.
|
|
11
|
+
*
|
|
12
|
+
* The originals are always called with the same arguments and their results returned, so the
|
|
13
|
+
* response behaves exactly as without PeekInside.
|
|
14
|
+
*/
|
|
15
|
+
export class ResponseTap {
|
|
16
|
+
/**
|
|
17
|
+
* Starts observing `res`.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('http').ServerResponse} res
|
|
20
|
+
* @param {number} limit - Most body bytes kept
|
|
21
|
+
*/
|
|
22
|
+
constructor(res, limit) {
|
|
23
|
+
/**
|
|
24
|
+
* Bytes the app has written so far
|
|
25
|
+
* @type {BodyCapture}
|
|
26
|
+
*/
|
|
27
|
+
this.body = new BodyCapture(limit);
|
|
28
|
+
this._res = res;
|
|
29
|
+
this._explicitHeaders = null;
|
|
30
|
+
|
|
31
|
+
this._tapBody(res);
|
|
32
|
+
this._tapWriteHead(res);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Response headers: those set with setHeader(), plus those passed to writeHead().
|
|
37
|
+
*
|
|
38
|
+
* @returns {Object<string, string|string[]|number>} Lowercase names
|
|
39
|
+
*/
|
|
40
|
+
headers() {
|
|
41
|
+
const res = this._res;
|
|
42
|
+
const current = typeof res.getHeaders === 'function' ? res.getHeaders() : {};
|
|
43
|
+
return mergeHeaders(current, this._explicitHeaders);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Writes after end(), including a second end(), send nothing, so they are not part of the body.
|
|
47
|
+
_tapBody(res) {
|
|
48
|
+
const body = this.body;
|
|
49
|
+
const originalWrite = res.write;
|
|
50
|
+
const originalEnd = res.end;
|
|
51
|
+
|
|
52
|
+
res.write = function (chunk, encoding) {
|
|
53
|
+
if (!res.writableEnded) {
|
|
54
|
+
body.push(chunk, encoding);
|
|
55
|
+
}
|
|
56
|
+
return originalWrite.apply(this, arguments);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
res.end = function (chunk, encoding) {
|
|
60
|
+
if (!res.writableEnded) {
|
|
61
|
+
body.push(chunk, encoding);
|
|
62
|
+
}
|
|
63
|
+
return originalEnd.apply(this, arguments);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// writeHead(status, [reason], [headers]): headers may be an object or a flat or nested array.
|
|
68
|
+
_tapWriteHead(res) {
|
|
69
|
+
const tap = this;
|
|
70
|
+
const originalWriteHead = res.writeHead;
|
|
71
|
+
if (typeof originalWriteHead !== 'function') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
res.writeHead = function (statusCode, reason, headers) {
|
|
76
|
+
const provided = reason !== null && typeof reason === 'object' ? reason : headers;
|
|
77
|
+
if (provided) {
|
|
78
|
+
tap._explicitHeaders = mergeHeaders(tap._explicitHeaders, normalizeHeaders(provided));
|
|
79
|
+
}
|
|
80
|
+
return originalWriteHead.apply(this, arguments);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `peekinside` command, which manages the free local dashboard:
|
|
3
|
+
*
|
|
4
|
+
* npx peekinside Start it in the background (if needed) and print its address
|
|
5
|
+
* npx peekinside open Same, then open it in the browser
|
|
6
|
+
* npx peekinside status Show whether it runs and which projects it holds
|
|
7
|
+
* npx peekinside stop Stop it (data stays on disk)
|
|
8
|
+
* npx peekinside run Run it in this terminal, with its log
|
|
9
|
+
* npx peekinside test Send a test error, to check that a project is set up
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { LOG_PREFIX, VERSION, settings } from './config.js';
|
|
16
|
+
import { HubClient } from './hub/client.js';
|
|
17
|
+
import { HubProcess } from './hub/process.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs one command against the hub on the configured port.
|
|
21
|
+
*/
|
|
22
|
+
export class Cli {
|
|
23
|
+
/**
|
|
24
|
+
* Targets the hub on PEEKINSIDE_HUB_PORT (default 7335).
|
|
25
|
+
*/
|
|
26
|
+
constructor() {
|
|
27
|
+
this.port = settings.hubPort();
|
|
28
|
+
this.url = settings.hubUrl(this.port);
|
|
29
|
+
this.hub = new HubClient(this.port);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs a command. Unknown commands print the help and exit with status 1, as do failures.
|
|
34
|
+
*
|
|
35
|
+
* @param {string[]} args - Command-line arguments, without node and the script
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
run(args) {
|
|
39
|
+
const command = args[0] || 'start';
|
|
40
|
+
const commands = {
|
|
41
|
+
start: () => this.start(false),
|
|
42
|
+
open: () => this.start(true),
|
|
43
|
+
status: () => this.status(),
|
|
44
|
+
stop: () => this.stop(),
|
|
45
|
+
run: () => new HubProcess().run(),
|
|
46
|
+
test: () => this.test(),
|
|
47
|
+
help: () => this.help(),
|
|
48
|
+
'--help': () => this.help(),
|
|
49
|
+
'-h': () => this.help(),
|
|
50
|
+
'--version': () => console.log(VERSION),
|
|
51
|
+
'-v': () => console.log(VERSION)
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!commands[command]) {
|
|
55
|
+
console.error(`Unknown command: ${command}\n`);
|
|
56
|
+
this.help();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
Promise.resolve(commands[command]()).catch((error) => {
|
|
60
|
+
console.error(error.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Starts the hub in the background unless it already runs, and prints its address.
|
|
67
|
+
*
|
|
68
|
+
* @param {boolean} openBrowser - Also open the dashboard in the default browser
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
* @throws {Error} When the hub did not start in time
|
|
71
|
+
*/
|
|
72
|
+
async start(openBrowser) {
|
|
73
|
+
let running = await this.hub.status();
|
|
74
|
+
if (!running) {
|
|
75
|
+
this.hub.spawn();
|
|
76
|
+
running = await this.hub.waitUntilReady(8000);
|
|
77
|
+
if (!running) {
|
|
78
|
+
throw new Error(`The local dashboard did not start. See ${settings.dataDir()}/hub.log`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(`PeekInside Local is running: ${this.url}`);
|
|
82
|
+
if (openBrowser) this._openInBrowser(this.url);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Prints whether the hub runs, and its projects.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async status() {
|
|
91
|
+
const running = await this.hub.status();
|
|
92
|
+
if (!running) {
|
|
93
|
+
console.log(`PeekInside Local is not running. Start it with: npx peekinside`);
|
|
94
|
+
console.log(`Data folder: ${settings.dataDir()}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { projects } = await this.hub.request('GET', '/api/projects');
|
|
98
|
+
console.log(`PeekInside Local ${running.version} is running: ${this.url} (pid ${running.pid})`);
|
|
99
|
+
console.log(`Data folder: ${running.dataDir}\n`);
|
|
100
|
+
if (!projects.length) {
|
|
101
|
+
console.log('No projects yet. Attach PeekInside to an app and start it.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const project of projects) {
|
|
105
|
+
const state = project.online
|
|
106
|
+
? `running${project.instances[0] && project.instances[0].port ? ` on :${project.instances[0].port}` : ''}`
|
|
107
|
+
: 'stopped';
|
|
108
|
+
const exceptions = Number.isInteger(project.exceptionCount) ? `${String(project.exceptionCount).padStart(5)} exceptions ` : '';
|
|
109
|
+
console.log(` ${project.online ? '●' : '○'} ${project.name.padEnd(28)} ${String(project.requestCount).padStart(6)} requests ${exceptions} ${state}`);
|
|
110
|
+
console.log(` ${project.root}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stops the hub and waits up to five seconds for it to exit.
|
|
116
|
+
*
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
* @throws {Error} When it is still running after that
|
|
119
|
+
*/
|
|
120
|
+
async stop() {
|
|
121
|
+
const running = await this.hub.status();
|
|
122
|
+
if (!running) {
|
|
123
|
+
console.log('PeekInside Local is not running.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
process.kill(running.pid, 'SIGTERM');
|
|
127
|
+
for (let i = 0; i < 50; i++) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
129
|
+
if (!(await this.hub.status().catch(() => null))) {
|
|
130
|
+
console.log('PeekInside Local stopped. Your data stays in ' + running.dataDir);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`PeekInside Local (pid ${running.pid}) did not stop.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sends a test error the way the app would, run from the project's folder: to PeekInside Cloud
|
|
139
|
+
* when PEEKINSIDE_API_KEY is set (in the environment, or in the project's .env), otherwise to the
|
|
140
|
+
* local dashboard, which is started if needed. Exits with status 1 when it didn't arrive.
|
|
141
|
+
*
|
|
142
|
+
* @returns {Promise<void>}
|
|
143
|
+
* @throws {Error} When the error could not be delivered
|
|
144
|
+
*/
|
|
145
|
+
async test() {
|
|
146
|
+
loadProjectEnv(process.cwd());
|
|
147
|
+
const { default: PeekInside } = await import('./PeekInside.js');
|
|
148
|
+
const cloud = Boolean(settings.apiKey());
|
|
149
|
+
if (!cloud) await this.start(false);
|
|
150
|
+
|
|
151
|
+
// Only report(): no automatic capture of this command's own errors, no instrumentation.
|
|
152
|
+
const peekInside = new PeekInside({ local: true, captureExceptions: false, timeline: false, projectRoot: process.cwd() });
|
|
153
|
+
const transport = peekInside._transport;
|
|
154
|
+
if (!transport) throw new Error('PeekInside could not set up a destination for the test error.');
|
|
155
|
+
if (peekInside._local) {
|
|
156
|
+
// This command prints where the error went; the app's "Requests from ..." line would only confuse.
|
|
157
|
+
peekInside._local.announced = true;
|
|
158
|
+
peekInside._local.start();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const id = peekInside.report(new PeekInsideTestError(), {
|
|
162
|
+
request: { method: 'GET', url: '/peekinside/test', headers: { host: 'localhost', 'user-agent': `peekinside-cli/${VERSION}` } },
|
|
163
|
+
tags: { test: 'true' },
|
|
164
|
+
extra: { sentBy: 'npx peekinside test', node: process.version, release: peekInside.release },
|
|
165
|
+
handled: true
|
|
166
|
+
});
|
|
167
|
+
if (!id) throw new Error('PeekInside is paused or not recording, so no test error was sent.');
|
|
168
|
+
|
|
169
|
+
const deadline = Date.now() + 15_000;
|
|
170
|
+
while ((transport.queue.length || transport.inflight) && !transport.disabled && Date.now() < deadline) {
|
|
171
|
+
await transport.flush();
|
|
172
|
+
if (transport.queue.length) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
173
|
+
}
|
|
174
|
+
const delivered = !transport.disabled && !transport.queue.length && !transport.inflight;
|
|
175
|
+
peekInside.stop();
|
|
176
|
+
if (!delivered) {
|
|
177
|
+
throw new Error(cloud
|
|
178
|
+
? 'The test error did not reach PeekInside Cloud. Check PEEKINSIDE_API_KEY and your connection.'
|
|
179
|
+
: `The test error did not reach the local dashboard. See ${settings.dataDir()}/hub.log`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const where = cloud
|
|
183
|
+
? `PeekInside Cloud (${transport.environment}). Open your project's Exceptions page: ${settings.siteUrl()}/projects`
|
|
184
|
+
: `PeekInside Local: ${this.url}/p/${peekInside._local.project.id}/exceptions`;
|
|
185
|
+
console.log(`${LOG_PREFIX} Sent a test error to ${where}`);
|
|
186
|
+
if (peekInside.release) console.log(`${LOG_PREFIX} Release: ${peekInside.release}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Prints the usage.
|
|
191
|
+
*
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
help() {
|
|
195
|
+
console.log(`PeekInside ${VERSION}: the free local dashboard for all your Node.js projects
|
|
196
|
+
|
|
197
|
+
Usage: npx peekinside [command]
|
|
198
|
+
|
|
199
|
+
start Start the dashboard in the background and print its address (default)
|
|
200
|
+
open Start it and open it in your browser
|
|
201
|
+
status Show whether it is running and list your projects
|
|
202
|
+
stop Stop it; your data stays in ${settings.dataDir()}
|
|
203
|
+
run Run it in this terminal
|
|
204
|
+
test Send a test error from this project, to check it's set up
|
|
205
|
+
|
|
206
|
+
Apps that use PeekInside start the dashboard automatically.
|
|
207
|
+
Settings: PEEKINSIDE_HUB_PORT (default 7335), PEEKINSIDE_HOME (default ~/.peekinside)`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Opening a browser is a convenience: when it fails, the address is printed instead.
|
|
211
|
+
_openInBrowser(target) {
|
|
212
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
213
|
+
? ['open', [target]]
|
|
214
|
+
: process.platform === 'win32'
|
|
215
|
+
? ['cmd', ['/c', 'start', '', target]]
|
|
216
|
+
: ['xdg-open', [target]];
|
|
217
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
218
|
+
child.on('error', () => console.log(`Open ${target} in your browser.`));
|
|
219
|
+
child.unref();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The error `npx peekinside test` sends: a stand-in for a real one, with a name of its own so it
|
|
225
|
+
* groups into one issue however often it is sent.
|
|
226
|
+
*/
|
|
227
|
+
class PeekInsideTestError extends Error {
|
|
228
|
+
constructor() {
|
|
229
|
+
super('This is a test error sent by `npx peekinside test`: errors from this project reach PeekInside.');
|
|
230
|
+
this.name = 'PeekInsideTestError';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// PEEKINSIDE_* settings from the project's .env files, for the variables the environment doesn't
|
|
235
|
+
// set already: apps usually load PEEKINSIDE_API_KEY from .env, and so does this command. Nothing
|
|
236
|
+
// else in those files is read into the environment.
|
|
237
|
+
function loadProjectEnv(dir) {
|
|
238
|
+
for (const name of ['.env.local', '.env']) {
|
|
239
|
+
let text;
|
|
240
|
+
try {
|
|
241
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const line of text.split(/\r?\n/)) {
|
|
246
|
+
const match = /^\s*(?:export\s+)?(PEEKINSIDE_[A-Z_]+)\s*=\s*(.*?)\s*$/.exec(line);
|
|
247
|
+
if (!match || process.env[match[1]] !== undefined) continue;
|
|
248
|
+
let value = match[2];
|
|
249
|
+
const quoted = /^(['"])(.*)\1$/.exec(value);
|
|
250
|
+
value = quoted ? quoted[2] : value.replace(/\s+#.*$/, '');
|
|
251
|
+
process.env[match[1]] = value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings shared by the SDK, the local hub and the CLI: product constants, and the environment
|
|
3
|
+
* variables that decide where requests go.
|
|
4
|
+
*
|
|
5
|
+
* Environment variables are read each time a setting is used, never cached, so a process (or a
|
|
6
|
+
* test) can change them at runtime.
|
|
7
|
+
*
|
|
8
|
+
* PEEKINSIDE_URL Base URL of the PeekInside site: cloud ingest and the dashboard interface
|
|
9
|
+
* (default https://peekinside.dev). Change it when developing the dashboard.
|
|
10
|
+
* PEEKINSIDE_HOME Folder for local data (default ~/.peekinside)
|
|
11
|
+
* PEEKINSIDE_HUB_PORT Port of the local dashboard (default 7335)
|
|
12
|
+
* PEEKINSIDE_MAX_REQUESTS Requests the local dashboard keeps per project (default 1000)
|
|
13
|
+
* PEEKINSIDE_MAX_EXCEPTIONS Exceptions the local dashboard keeps per project (default 500)
|
|
14
|
+
* PEEKINSIDE_API_KEY PeekInside Cloud API key; turns on cloud mode
|
|
15
|
+
* PEEKINSIDE_ENV Environment label (falls back to NODE_ENV)
|
|
16
|
+
* PEEKINSIDE_PROJECT Project name in the local dashboard
|
|
17
|
+
* PEEKINSIDE_LOCAL 1 or 0 to force the local dashboard on or off
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
export const PRODUCT_NAME = 'PeekInside';
|
|
26
|
+
export const LOG_PREFIX = '[peekinside]';
|
|
27
|
+
export const DEFAULT_SITE_URL = 'https://peekinside.dev';
|
|
28
|
+
// "PEEK" on a phone keypad
|
|
29
|
+
export const DEFAULT_HUB_PORT = 7335;
|
|
30
|
+
// Bumped only for breaking changes to the hub's HTTP API; the dashboard interface is versioned with it.
|
|
31
|
+
export const HUB_API_VERSION = 1;
|
|
32
|
+
export const HUB_APP_ID = 'peekinside-hub';
|
|
33
|
+
|
|
34
|
+
export const VERSION = readPackageVersion();
|
|
35
|
+
|
|
36
|
+
const TRUE_FLAGS = ['1', 'true', 'yes', 'on'];
|
|
37
|
+
const FALSE_FLAGS = ['0', 'false', 'no', 'off'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reads PeekInside's settings from environment variables.
|
|
41
|
+
*
|
|
42
|
+
* Every method reads the variables again, so changes made after startup apply.
|
|
43
|
+
*/
|
|
44
|
+
export class Settings {
|
|
45
|
+
/**
|
|
46
|
+
* @param {Object<string, string>} [env] - Variables to read. By default process.env, looked up on
|
|
47
|
+
* every read.
|
|
48
|
+
*/
|
|
49
|
+
constructor(env = null) {
|
|
50
|
+
this._env = env;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The variables settings are read from.
|
|
55
|
+
*
|
|
56
|
+
* @type {Object<string, string>}
|
|
57
|
+
*/
|
|
58
|
+
get env() {
|
|
59
|
+
return this._env || process.env;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Origin of the PeekInside site, which hosts cloud ingest and the dashboard interface.
|
|
64
|
+
* An invalid PEEKINSIDE_URL is reported and ignored, so a typo can't send data elsewhere.
|
|
65
|
+
*
|
|
66
|
+
* @returns {string} Origin such as "https://peekinside.dev"
|
|
67
|
+
*/
|
|
68
|
+
siteUrl() {
|
|
69
|
+
const value = this.env.PEEKINSIDE_URL;
|
|
70
|
+
if (!value) return DEFAULT_SITE_URL;
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(value);
|
|
73
|
+
if (url.protocol === 'https:' || url.protocol === 'http:') return url.origin;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Fall through to the default.
|
|
76
|
+
}
|
|
77
|
+
console.warn(`${LOG_PREFIX} Ignoring PEEKINSIDE_URL: "${value}" is not an http(s) URL`);
|
|
78
|
+
return DEFAULT_SITE_URL;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Folder of the local dashboard's data.
|
|
83
|
+
*
|
|
84
|
+
* @returns {string} Absolute path
|
|
85
|
+
*/
|
|
86
|
+
dataDir() {
|
|
87
|
+
return path.resolve(this.env.PEEKINSIDE_HOME || path.join(os.homedir(), '.peekinside'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Port of the local dashboard; invalid values fall back to the default.
|
|
92
|
+
*
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
hubPort() {
|
|
96
|
+
const port = Number(this.env.PEEKINSIDE_HUB_PORT);
|
|
97
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_HUB_PORT;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Address of the local dashboard, as shown to people.
|
|
102
|
+
*
|
|
103
|
+
* @param {number} [port] - Hub port (default: hubPort())
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
hubUrl(port = this.hubPort()) {
|
|
107
|
+
return `http://localhost:${port}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Requests the local dashboard keeps per project.
|
|
112
|
+
*
|
|
113
|
+
* @returns {number|undefined} The configured number, or undefined for the store's default
|
|
114
|
+
*/
|
|
115
|
+
maxRequests() {
|
|
116
|
+
return Number(this.env.PEEKINSIDE_MAX_REQUESTS) || undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Exceptions the local dashboard keeps per project.
|
|
121
|
+
*
|
|
122
|
+
* @returns {number|undefined} The configured number, or undefined for the store's default
|
|
123
|
+
*/
|
|
124
|
+
maxExceptions() {
|
|
125
|
+
return Number(this.env.PEEKINSIDE_MAX_EXCEPTIONS) || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @returns {string|undefined} PeekInside Cloud API key
|
|
130
|
+
*/
|
|
131
|
+
apiKey() {
|
|
132
|
+
return this.env.PEEKINSIDE_API_KEY;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @returns {string|undefined} Environment label: PEEKINSIDE_ENV, then NODE_ENV
|
|
137
|
+
*/
|
|
138
|
+
environment() {
|
|
139
|
+
return this.env.PEEKINSIDE_ENV || this.env.NODE_ENV;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @returns {string|undefined} Project name chosen for the local dashboard
|
|
144
|
+
*/
|
|
145
|
+
projectName() {
|
|
146
|
+
return this.env.PEEKINSIDE_PROJECT;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether the free local dashboard is on when the app doesn't decide. It is for development,
|
|
151
|
+
* so it is off in production, in tests and in CI, unless PEEKINSIDE_LOCAL says otherwise.
|
|
152
|
+
*
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
localMode() {
|
|
156
|
+
const flag = String(this.env.PEEKINSIDE_LOCAL || '').trim().toLowerCase();
|
|
157
|
+
if (TRUE_FLAGS.includes(flag)) return true;
|
|
158
|
+
if (FALSE_FLAGS.includes(flag)) return false;
|
|
159
|
+
|
|
160
|
+
const ci = String(this.env.CI || '').toLowerCase();
|
|
161
|
+
const inCi = ci !== '' && ci !== '0' && ci !== 'false';
|
|
162
|
+
return this.env.NODE_ENV !== 'production' && this.env.NODE_ENV !== 'test' && !inCi;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @returns {boolean} Whether NODE_ENV is "production"
|
|
167
|
+
*/
|
|
168
|
+
isProduction() {
|
|
169
|
+
return this.env.NODE_ENV === 'production';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Settings read from process.env, shared by every module. */
|
|
174
|
+
export const settings = new Settings();
|
|
175
|
+
|
|
176
|
+
function readPackageVersion() {
|
|
177
|
+
try {
|
|
178
|
+
const file = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
179
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')).version || '';
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
}
|