@keepsnappy/agent 0.0.0-stage → 0.1.0-alpha.11

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.
@@ -0,0 +1,124 @@
1
+ // Posts the step markers step.js recorded during the iteration, after it ended (end-run.yaml), so sending them never
2
+ // changes the test's pace. Farm mode only: POST <api>/v1/markers with the marker's own run token as Bearer (local mode
3
+ // posts at once from step.js). ES5 only (older Maestro builds run Rhino). Must not throw.
4
+ // One request per token carries all its markers ({markers: [...]}, at most 500 each), so a long journey stays far
5
+ // under the API's per-run rate limit; a request that fails on the network, 429 or 5xx is tried 3 times (Retry-After
6
+ // honoured, else 1 s then 2 s). An API from before batches answers 400 without per-item results: the markers then go
7
+ // one by one, as they used to.
8
+ // Clock round trips (plan 51 G1.4): each answer carries the API's receive/send times; that trip rides with the next
9
+ // request of the same token, and what is left goes to <api>/v1/clock at the end, as a short chain of CLOCK_PROBES
10
+ // requests whose answers are trips too, so a token still reports several round trips although its markers now go in
11
+ // one request. A trip whose POST failed or came back without a clock (an older API) stays pending, so nothing
12
+ // measured is dropped.
13
+ try {
14
+ var g = typeof globalThis !== 'undefined' ? globalThis : this;
15
+ var pick = function (name, dflt) {
16
+ var modern = name.replace(/^PERFLAB_/, 'KEEPSNAPPY_');
17
+ var names = modern === name ? [name, 'MAESTRO_' + name] : [modern, 'MAESTRO_' + modern, name, 'MAESTRO_' + name];
18
+ for (var i = 0; i < names.length; i++) {
19
+ var v = g[names[i]];
20
+ if (typeof v === 'string' && (v !== '' || names[i].indexOf('KEEPSNAPPY_') >= 0)) return v;
21
+ }
22
+ return dflt;
23
+ };
24
+ var MAX_SAMPLES = 16;
25
+ var PER_REQUEST = 500;
26
+ var TRIES = 3;
27
+ var RETRY_CAP_MS = 30000;
28
+ var CLOCK_PROBES = 4;
29
+ var out = typeof output !== 'undefined' && output ? output : {};
30
+ var list = out.perflabMarkers || [];
31
+ out.perflabMarkers = [];
32
+ var api = pick('PERFLAB_API_URL', typeof out.perflabApi === 'string' ? out.perflabApi : '').replace(/\/+$/, '');
33
+ var wait = function (ms) {
34
+ try {
35
+ java.lang.Thread.sleep(ms);
36
+ return;
37
+ } catch (e0) {
38
+ // no Java bridge: wait on the clock
39
+ }
40
+ var until = Date.now() + ms;
41
+ while (Date.now() < until) {
42
+ // nothing
43
+ }
44
+ };
45
+ var header = function (res, name) {
46
+ var h = res && res.headers;
47
+ if (!h) return null;
48
+ var v = h[name] !== undefined ? h[name] : h[name.toLowerCase()];
49
+ return v === undefined || v === null ? null : String(v);
50
+ };
51
+ // One POST: the answer's status and parsed body, the clock trip when the answer has one; null when it threw.
52
+ var postOnce = function (path, token, body) {
53
+ var t0 = Date.now();
54
+ var res = http.post(api + path, {
55
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
56
+ body: JSON.stringify(body),
57
+ });
58
+ var t3 = Date.now();
59
+ var parsed = null;
60
+ try {
61
+ parsed = res && res.body ? JSON.parse(res.body) : null;
62
+ } catch (e1) {
63
+ // not JSON
64
+ }
65
+ var c = parsed ? parsed.clock : null;
66
+ var trip = c && typeof c.recv_ms === 'number' && typeof c.send_ms === 'number' ? { t0: t0, t1: c.recv_ms, t2: c.send_ms, t3: t3 } : null;
67
+ return { status: res && typeof res.status === 'number' ? res.status : null, body: parsed, trip: trip, retryAfter: Number(header(res, 'Retry-After')) };
68
+ };
69
+ var postRetry = function (path, token, body) {
70
+ var a = null;
71
+ for (var attempt = 0; attempt < TRIES; attempt++) {
72
+ if (attempt > 0) wait(Math.min(a && a.retryAfter > 0 ? a.retryAfter * 1000 : 1000 * Math.pow(2, attempt - 1), RETRY_CAP_MS));
73
+ try {
74
+ a = postOnce(path, token, body);
75
+ } catch (e2) {
76
+ a = null;
77
+ }
78
+ if (a && a.status !== 429 && !(a.status >= 500)) return a;
79
+ }
80
+ return a;
81
+ };
82
+ var pending = {};
83
+ var tokens = [];
84
+ var byToken = {};
85
+ for (var k = 0; k < list.length && api !== ''; k++) {
86
+ var m = list[k];
87
+ if (!m || typeof m.token !== 'string' || m.token === '') continue;
88
+ if (!byToken[m.token]) {
89
+ byToken[m.token] = [];
90
+ pending[m.token] = [];
91
+ tokens.push(m.token);
92
+ }
93
+ byToken[m.token].push({ step: m.step, phase: m.phase, t_host: m.t_host });
94
+ }
95
+ var send = function (token, body) {
96
+ if (pending[token].length) body.clock = pending[token].slice(-MAX_SAMPLES);
97
+ var a = postRetry('/v1/markers', token, body);
98
+ if (a && a.trip) pending[token] = [a.trip];
99
+ return a;
100
+ };
101
+ for (var t = 0; t < tokens.length; t++) {
102
+ var mine = byToken[tokens[t]];
103
+ for (var i = 0; i < mine.length; i += PER_REQUEST) {
104
+ var chunk = mine.slice(i, i + PER_REQUEST);
105
+ var a = send(tokens[t], { markers: chunk });
106
+ if (!a || a.status !== 400 || (a.body && a.body.results)) continue;
107
+ for (var j = 0; j < chunk.length; j++) send(tokens[t], chunk[j]);
108
+ }
109
+ }
110
+ for (var p = 0; p < tokens.length; p++) {
111
+ for (var n = 0; n < CLOCK_PROBES && pending[tokens[p]].length; n++) {
112
+ try {
113
+ var probe = postOnce('/v1/clock', tokens[p], { source: 'host', samples: pending[tokens[p]].slice(-MAX_SAMPLES) });
114
+ if (!probe.trip) break;
115
+ pending[tokens[p]] = [probe.trip];
116
+ } catch (e3) {
117
+ // an older API has no /v1/clock
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ } catch (e) {
123
+ // swallowed on purpose
124
+ }
@@ -0,0 +1,39 @@
1
+ // Declared step marker. Runs on the Maestro HOST, so it never touches the device. ES5 only (older Maestro builds run
2
+ // Rhino). Must not throw: a marker is never a test failure.
3
+ // Inputs: STEP, PHASE (from step.yaml). t_host is taken now, when the action happens, whenever the marker is sent.
4
+ // Farm mode (`perflab farm open`): the flow has set `output.perflabToken` (one run token per iteration, from the
5
+ // generated tokens.js) and the API base comes from `output.perflabApi` or PERFLAB_API_URL. The marker is only recorded
6
+ // in the flow's memory; send-markers.js (end-run.yaml) posts the iteration's markers to <api>/v1/markers after it
7
+ // ended, so the test never waits on the network between actions.
8
+ // Local mode (`perflab run` / `measure`): PERFLAB_MARKER_URL, PERFLAB_RUN_ID point at the CLI's own endpoint on this
9
+ // host, posted at once (local flows have no end-run step). Maestro only forwards host env vars with the MAESTRO_ prefix,
10
+ // unprefixed, so both spellings are read (docs/research/28).
11
+ try {
12
+ var g = typeof globalThis !== 'undefined' ? globalThis : this;
13
+ var pick = function (name, dflt) {
14
+ var modern = name.replace(/^PERFLAB_/, 'KEEPSNAPPY_');
15
+ var names = modern === name ? [name, 'MAESTRO_' + name] : [modern, 'MAESTRO_' + modern, name, 'MAESTRO_' + name];
16
+ for (var i = 0; i < names.length; i++) {
17
+ var v = g[names[i]];
18
+ if (typeof v === 'string' && (v !== '' || names[i].indexOf('KEEPSNAPPY_') >= 0)) return v;
19
+ }
20
+ return dflt;
21
+ };
22
+ var out = typeof output !== 'undefined' && output ? output : {};
23
+ var step = pick('STEP', '');
24
+ var phase = pick('PHASE', '');
25
+ var token = typeof out.perflabToken === 'string' ? out.perflabToken : '';
26
+ var api = pick('PERFLAB_API_URL', typeof out.perflabApi === 'string' ? out.perflabApi : '');
27
+ if (token !== '' && api !== '') {
28
+ out.perflabMarkers = out.perflabMarkers || [];
29
+ out.perflabMarkers.push({ token: token, step: step, phase: phase, t_host: Date.now() });
30
+ } else {
31
+ var base = pick('PERFLAB_MARKER_URL', 'http://127.0.0.1:47321');
32
+ http.post(base + '/marker', {
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({ run: pick('PERFLAB_RUN_ID', ''), step: step, phase: phase, t_host: Date.now() }),
35
+ });
36
+ }
37
+ } catch (e) {
38
+ // swallowed on purpose
39
+ }
@@ -0,0 +1,11 @@
1
+ # PerfLab step marker. Call with env STEP (name) and PHASE (start|end):
2
+ # - runFlow: { file: perflab/step.yaml, env: { STEP: chat.open, PHASE: start } }
3
+ # Never fails the surrounding flow, even when no PerfLab CLI is listening. On the farm it only records the marker:
4
+ # run send-markers.js at the end of each iteration (examples/farm/flows/perflab/end-run.yaml).
5
+ appId: any
6
+ ---
7
+ - runScript:
8
+ file: step.js
9
+ env:
10
+ STEP: ${STEP}
11
+ PHASE: ${PHASE}
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@keepsnappy/agent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.11",
4
+ "description": "KeepSnappy CLI: runs your app journeys, measures from inside the app and reports More work / Less work / No change per step.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/arekkubaczkowski/keepsnappy.git",
10
+ "directory": "apps/agent"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "bin": {
19
+ "keepsnappy": "dist/keepsnappy.js",
20
+ "perflab": "dist/keepsnappy.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "maestro",
25
+ "examples",
26
+ "README.md"
27
+ ]
28
+ }