@gurulu/cli 0.4.0 → 0.4.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/dist/commands/add-server.js +13 -6
- package/dist/commands/alerts.d.ts +5 -0
- package/dist/commands/alerts.js +43 -15
- package/dist/commands/audiences.d.ts +3 -0
- package/dist/commands/audiences.js +34 -7
- package/dist/commands/events.d.ts +6 -0
- package/dist/commands/events.js +182 -1
- package/dist/commands/experiments.d.ts +4 -0
- package/dist/commands/experiments.js +46 -15
- package/dist/commands/funnels.d.ts +17 -0
- package/dist/commands/funnels.js +203 -0
- package/dist/commands/goals.d.ts +18 -0
- package/dist/commands/goals.js +214 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +57 -1
- package/dist/commands/sourcemap.d.ts +17 -5
- package/dist/commands/sourcemap.js +73 -6
- package/dist/commands/watch.d.ts +45 -0
- package/dist/commands/watch.js +258 -0
- package/dist/frameworks/detect.js +29 -7
- package/dist/index.js +158 -13
- package/package.json +1 -1
- package/scripts/gurulu-agentic-install.mjs +225 -0
- package/scripts/gurulu-scan.lib.cjs +539 -19
- package/scripts/patches/auto-instrument/hono.cjs +381 -0
- package/scripts/patches/auto-instrument/index.cjs +2 -0
- package/scripts/patches/auto-instrument/nextjs-app-router.cjs +13 -4
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `gurulu sourcemap upload` — Upload source maps for error deobfuscation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `/api/errors/sourcemaps` with Bearer auth. The server stores them on
|
|
7
|
-
* disk for later stack trace deobfuscation (follow-up task).
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
5
|
+
* Web (default):
|
|
10
6
|
* gurulu sourcemap upload --release v1.0.0 --dir .next/static/
|
|
7
|
+
*
|
|
8
|
+
* Native (C3 — iOS dSYM / Android ProGuard):
|
|
9
|
+
* gurulu sourcemap upload --platform=ios --version=1.4.2 --bundle-id=com.example.app --file=app.xcarchive/dSYMs/MyApp.dSYM.zip
|
|
10
|
+
* gurulu sourcemap upload --platform=android --version=1.4.2 --bundle-id=com.example.app --file=app/build/outputs/mapping/release/mapping.txt
|
|
11
|
+
*
|
|
12
|
+
* The native variant POSTs a single `file` to `/api/sourcemap/native`, which
|
|
13
|
+
* persists it under {SOURCEMAP_STORAGE_PATH}/native/{siteId}/{platform}/{release}/
|
|
14
|
+
* and registers a `NativeSymbolFile` row.
|
|
11
15
|
*/
|
|
12
16
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
17
|
if (k2 === undefined) k2 = k;
|
|
@@ -55,10 +59,71 @@ async function sourcemapCommand(args) {
|
|
|
55
59
|
if (action !== 'upload') {
|
|
56
60
|
(0, ui_1.error)(`Unknown sourcemap action: ${action}`);
|
|
57
61
|
(0, ui_1.info)('Usage: gurulu sourcemap upload --release <version> --dir <path>');
|
|
62
|
+
(0, ui_1.info)(' gurulu sourcemap upload --platform=ios|android --version=<v> --bundle-id=<id> --file=<path>');
|
|
58
63
|
process.exit(1);
|
|
59
64
|
}
|
|
65
|
+
if (args.platform === 'ios' || args.platform === 'android') {
|
|
66
|
+
return uploadNativeCmd(args);
|
|
67
|
+
}
|
|
60
68
|
return uploadCmd(args);
|
|
61
69
|
}
|
|
70
|
+
async function uploadNativeCmd(args) {
|
|
71
|
+
const version = args.version || args.release;
|
|
72
|
+
if (!version) {
|
|
73
|
+
(0, ui_1.error)('Missing --version flag. Example: --version=1.4.2');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (!args.file) {
|
|
77
|
+
(0, ui_1.error)('Missing --file flag. For iOS pass the dSYM zip; for Android pass mapping.txt');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const bundleId = args.bundleId || args.appId;
|
|
81
|
+
if (!bundleId) {
|
|
82
|
+
(0, ui_1.error)('Missing --bundle-id flag (iOS bundleId or Android applicationId)');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const filePath = path.resolve(args.file);
|
|
86
|
+
if (!fs.existsSync(filePath)) {
|
|
87
|
+
(0, ui_1.error)(`File not found: ${filePath}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const profile = await (0, config_1.loadActiveProfile)({ profile: args.profile });
|
|
91
|
+
const siteId = args.site || profile.site_id;
|
|
92
|
+
if (!siteId) {
|
|
93
|
+
(0, ui_1.error)('Missing --site flag or default site in profile.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const fileName = path.basename(filePath);
|
|
97
|
+
const content = fs.readFileSync(filePath);
|
|
98
|
+
const blob = new Blob([content], { type: 'application/octet-stream' });
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.append('platform', args.platform);
|
|
101
|
+
formData.append('version', version);
|
|
102
|
+
formData.append('bundleId', bundleId);
|
|
103
|
+
formData.append('siteId', siteId);
|
|
104
|
+
formData.append('file', blob, fileName);
|
|
105
|
+
(0, ui_1.info)(`Uploading ${fileName} (${args.platform}) for ${bundleId}@${version}...`);
|
|
106
|
+
const res = await (0, api_client_1.cliApi)('/api/sourcemap/native', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: formData,
|
|
109
|
+
profile: args.profile,
|
|
110
|
+
headers: {},
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const body = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
114
|
+
(0, ui_1.error)(`Upload failed (${res.status}): ${body.error || res.statusText}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const result = await res.json();
|
|
118
|
+
if (args.json) {
|
|
119
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
(0, ui_1.success)(`Native symbols uploaded for ${args.platform} ${version}`);
|
|
123
|
+
process.stdout.write((0, ui_1.dim)(` File: ${result.fileName}\n`));
|
|
124
|
+
process.stdout.write((0, ui_1.dim)(` Path: ${result.storagePath}\n`));
|
|
125
|
+
process.stdout.write((0, ui_1.dim)(` Size: ${result.sizeBytes} bytes\n`));
|
|
126
|
+
}
|
|
62
127
|
function findMapFiles(dir) {
|
|
63
128
|
const results = [];
|
|
64
129
|
function walk(d) {
|
|
@@ -103,17 +168,19 @@ async function uploadCmd(args) {
|
|
|
103
168
|
(0, ui_1.error)('Missing --site flag or default site in profile. Use: gurulu sourcemap upload --site <site-id> ...');
|
|
104
169
|
process.exit(1);
|
|
105
170
|
}
|
|
171
|
+
const platform = args.platform || 'web';
|
|
106
172
|
// Build FormData
|
|
107
173
|
const formData = new FormData();
|
|
108
174
|
formData.append('release', args.release);
|
|
109
175
|
formData.append('siteId', siteId);
|
|
176
|
+
formData.append('platform', platform);
|
|
110
177
|
for (const filePath of mapFiles) {
|
|
111
178
|
const content = fs.readFileSync(filePath);
|
|
112
179
|
const fileName = path.basename(filePath);
|
|
113
180
|
const blob = new Blob([content], { type: 'application/json' });
|
|
114
181
|
formData.append(fileName, blob, fileName);
|
|
115
182
|
}
|
|
116
|
-
const res = await (0, api_client_1.cliApi)('/api/
|
|
183
|
+
const res = await (0, api_client_1.cliApi)('/api/tracker-config/sourcemaps', {
|
|
117
184
|
method: 'POST',
|
|
118
185
|
body: formData,
|
|
119
186
|
profile: args.profile,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint B Group VII B14 — `gurulu watch` live event stream.
|
|
3
|
+
*
|
|
4
|
+
* Tail every event for the active tenant in near-real time, with optional
|
|
5
|
+
* filtering by site / event types. Uses the existing CLI SSE endpoint
|
|
6
|
+
* (/api/cli/events/tail) so we don't duplicate poll/heartbeat plumbing.
|
|
7
|
+
*
|
|
8
|
+
* gurulu watch # all sites, all events
|
|
9
|
+
* gurulu watch --site=site_123 # one site
|
|
10
|
+
* gurulu watch --types=$purchase,page_view # filter by event names (CSV)
|
|
11
|
+
* gurulu watch --tail=50 # show last N before streaming
|
|
12
|
+
* gurulu watch --json # one JSON line per event
|
|
13
|
+
*
|
|
14
|
+
* Auth: bearer token from active profile (`loadActiveProfile` via cliApi).
|
|
15
|
+
* Graceful shutdown: SIGINT cancels the fetch via AbortController.
|
|
16
|
+
*/
|
|
17
|
+
export interface WatchArgs {
|
|
18
|
+
site?: string;
|
|
19
|
+
types?: string;
|
|
20
|
+
tail?: number;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
profile?: string;
|
|
23
|
+
/** Override the upstream SSE path — for tests/proxies. Defaults to the CLI
|
|
24
|
+
* bearer-auth tail route. */
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
}
|
|
27
|
+
interface RealtimeEventRow {
|
|
28
|
+
event_id?: string;
|
|
29
|
+
event_ts?: string;
|
|
30
|
+
event_name?: string;
|
|
31
|
+
site_id?: string;
|
|
32
|
+
site_name?: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
page_url?: string;
|
|
35
|
+
anonymous_id?: string;
|
|
36
|
+
user_id?: string;
|
|
37
|
+
selector?: string;
|
|
38
|
+
revenue_value?: number | string;
|
|
39
|
+
revenue_currency?: string;
|
|
40
|
+
properties?: Record<string, unknown>;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
export declare function watchCommand(args: WatchArgs): Promise<void>;
|
|
44
|
+
export declare function formatEventLine(row: RealtimeEventRow, json: boolean): string;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sprint B Group VII B14 — `gurulu watch` live event stream.
|
|
4
|
+
*
|
|
5
|
+
* Tail every event for the active tenant in near-real time, with optional
|
|
6
|
+
* filtering by site / event types. Uses the existing CLI SSE endpoint
|
|
7
|
+
* (/api/cli/events/tail) so we don't duplicate poll/heartbeat plumbing.
|
|
8
|
+
*
|
|
9
|
+
* gurulu watch # all sites, all events
|
|
10
|
+
* gurulu watch --site=site_123 # one site
|
|
11
|
+
* gurulu watch --types=$purchase,page_view # filter by event names (CSV)
|
|
12
|
+
* gurulu watch --tail=50 # show last N before streaming
|
|
13
|
+
* gurulu watch --json # one JSON line per event
|
|
14
|
+
*
|
|
15
|
+
* Auth: bearer token from active profile (`loadActiveProfile` via cliApi).
|
|
16
|
+
* Graceful shutdown: SIGINT cancels the fetch via AbortController.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.watchCommand = watchCommand;
|
|
20
|
+
exports.formatEventLine = formatEventLine;
|
|
21
|
+
const api_client_1 = require("../api-client");
|
|
22
|
+
const ui_1 = require("../utils/ui");
|
|
23
|
+
async function watchCommand(args) {
|
|
24
|
+
const types = parseTypes(args.types);
|
|
25
|
+
const tailCount = Math.max(0, Number(args.tail) || 0);
|
|
26
|
+
// 1. Optional tail of recent events before streaming.
|
|
27
|
+
if (tailCount > 0) {
|
|
28
|
+
await printTail(args, types, tailCount);
|
|
29
|
+
}
|
|
30
|
+
// 2. Open the SSE stream and forward events until SIGINT.
|
|
31
|
+
await streamEvents(args, types);
|
|
32
|
+
}
|
|
33
|
+
function parseTypes(raw) {
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
const list = raw
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter((s) => s.length > 0);
|
|
40
|
+
if (list.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return new Set(list);
|
|
43
|
+
}
|
|
44
|
+
async function printTail(args, types, tailCount) {
|
|
45
|
+
const qs = new URLSearchParams();
|
|
46
|
+
if (args.site)
|
|
47
|
+
qs.set('site', args.site);
|
|
48
|
+
// The tail-history endpoint accepts a single event_name. When the user
|
|
49
|
+
// requested multiple, fetch the union and filter client-side.
|
|
50
|
+
if (types && types.size === 1) {
|
|
51
|
+
qs.set('event_name', Array.from(types)[0]);
|
|
52
|
+
}
|
|
53
|
+
qs.set('limit', String(Math.min(tailCount * (types ? 5 : 1), 500)));
|
|
54
|
+
qs.set('since', '24h');
|
|
55
|
+
const path = `/api/cli/events?${qs.toString()}`;
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = await (0, api_client_1.cliApiJson)(path, {
|
|
59
|
+
profile: args.profile,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
(0, ui_1.error)(`Failed to fetch tail history: ${err.message}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const all = body.events || [];
|
|
67
|
+
const filtered = (types ? all.filter((e) => e.event_name && types.has(e.event_name)) : all)
|
|
68
|
+
.slice(-tailCount) // keep the newest N (events arrive newest-first)
|
|
69
|
+
.reverse();
|
|
70
|
+
if (filtered.length === 0) {
|
|
71
|
+
if (!args.json)
|
|
72
|
+
(0, ui_1.info)('No prior events in the last 24h.');
|
|
73
|
+
}
|
|
74
|
+
for (const ev of filtered) {
|
|
75
|
+
process.stdout.write(formatEventLine(ev, !!args.json));
|
|
76
|
+
}
|
|
77
|
+
if (!args.json && filtered.length > 0) {
|
|
78
|
+
process.stdout.write((0, ui_1.dim)(' --- live stream ---\n'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function streamEvents(args, types) {
|
|
82
|
+
const qs = new URLSearchParams();
|
|
83
|
+
if (args.site)
|
|
84
|
+
qs.set('site', args.site);
|
|
85
|
+
// SSE backend accepts a single eventName. Multi-type filter is applied on
|
|
86
|
+
// the client. (Forwarded as a hint when only one is set so the server can
|
|
87
|
+
// skip irrelevant rows entirely.)
|
|
88
|
+
if (types && types.size === 1)
|
|
89
|
+
qs.set('event_name', Array.from(types)[0]);
|
|
90
|
+
const path = args.endpoint ||
|
|
91
|
+
`/api/cli/events/tail${qs.toString() ? `?${qs.toString()}` : ''}`;
|
|
92
|
+
// SIGINT → abort the fetch so the SSE socket is released cleanly.
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const onSigint = () => {
|
|
95
|
+
if (!args.json)
|
|
96
|
+
process.stderr.write((0, ui_1.dim)('\n[watch] disconnected\n'));
|
|
97
|
+
controller.abort();
|
|
98
|
+
// Give the abort path a tick to drain, then exit normally.
|
|
99
|
+
setTimeout(() => process.exit(0), 25);
|
|
100
|
+
};
|
|
101
|
+
process.on('SIGINT', onSigint);
|
|
102
|
+
process.on('SIGTERM', onSigint);
|
|
103
|
+
if (!args.json) {
|
|
104
|
+
(0, ui_1.info)(`Streaming events${args.site ? ` for site=${args.site}` : ''}${types ? ` types=[${Array.from(types).join(',')}]` : ''} — Ctrl+C to stop`);
|
|
105
|
+
}
|
|
106
|
+
// Reconnect loop — SSE streams cap at 10 minutes server-side. Honour the
|
|
107
|
+
// `reconnect` frame and any transient network drop.
|
|
108
|
+
let attempt = 0;
|
|
109
|
+
while (!controller.signal.aborted) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await (0, api_client_1.cliApi)(path, {
|
|
112
|
+
profile: args.profile,
|
|
113
|
+
headers: { accept: 'text/event-stream' },
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
if (!res.body) {
|
|
117
|
+
(0, ui_1.error)('Watch stream returned no body.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
attempt = 0; // reset backoff on successful connect
|
|
121
|
+
await consumeSseBody(res.body, types, !!args.json);
|
|
122
|
+
// Stream ended cleanly (max_lifetime). Re-open immediately.
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (controller.signal.aborted)
|
|
126
|
+
return;
|
|
127
|
+
attempt += 1;
|
|
128
|
+
const wait = Math.min(15_000, 500 * 2 ** attempt);
|
|
129
|
+
if (!args.json) {
|
|
130
|
+
process.stderr.write((0, ui_1.dim)(`[watch] connection lost (${err.message}); reconnecting in ${wait}ms\n`));
|
|
131
|
+
}
|
|
132
|
+
await sleep(wait);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function consumeSseBody(body, types, json) {
|
|
137
|
+
// Browser-style ReadableStream
|
|
138
|
+
const reader = body.getReader?.();
|
|
139
|
+
if (reader) {
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
let buffer = '';
|
|
142
|
+
// eslint-disable-next-line no-constant-condition
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done)
|
|
146
|
+
return;
|
|
147
|
+
buffer += decoder.decode(value, { stream: true });
|
|
148
|
+
const flushed = drainSseBuffer(buffer);
|
|
149
|
+
buffer = flushed.tail;
|
|
150
|
+
for (const frame of flushed.frames)
|
|
151
|
+
emitFrame(frame, types, json);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Node Readable fallback
|
|
155
|
+
let buffer = '';
|
|
156
|
+
for await (const chunk of body) {
|
|
157
|
+
buffer += Buffer.from(chunk).toString('utf8');
|
|
158
|
+
const flushed = drainSseBuffer(buffer);
|
|
159
|
+
buffer = flushed.tail;
|
|
160
|
+
for (const frame of flushed.frames)
|
|
161
|
+
emitFrame(frame, types, json);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function drainSseBuffer(buffer) {
|
|
165
|
+
const frames = [];
|
|
166
|
+
let idx = buffer.indexOf('\n\n');
|
|
167
|
+
while (idx !== -1) {
|
|
168
|
+
const block = buffer.slice(0, idx);
|
|
169
|
+
buffer = buffer.slice(idx + 2);
|
|
170
|
+
let event = 'message';
|
|
171
|
+
let data = '';
|
|
172
|
+
for (const line of block.split('\n')) {
|
|
173
|
+
if (line.startsWith('event: '))
|
|
174
|
+
event = line.slice(7).trim();
|
|
175
|
+
else if (line.startsWith('data: '))
|
|
176
|
+
data += line.slice(6);
|
|
177
|
+
}
|
|
178
|
+
if (data)
|
|
179
|
+
frames.push({ event, data });
|
|
180
|
+
idx = buffer.indexOf('\n\n');
|
|
181
|
+
}
|
|
182
|
+
return { frames, tail: buffer };
|
|
183
|
+
}
|
|
184
|
+
function emitFrame(frame, types, json) {
|
|
185
|
+
if (frame.event !== 'realtime_event')
|
|
186
|
+
return;
|
|
187
|
+
let row;
|
|
188
|
+
try {
|
|
189
|
+
row = JSON.parse(frame.data);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (types && row.event_name && !types.has(row.event_name))
|
|
195
|
+
return;
|
|
196
|
+
process.stdout.write(formatEventLine(row, json));
|
|
197
|
+
}
|
|
198
|
+
function formatEventLine(row, json) {
|
|
199
|
+
if (json)
|
|
200
|
+
return JSON.stringify(row) + '\n';
|
|
201
|
+
const ts = formatTimestamp(row.event_ts);
|
|
202
|
+
const name = String(row.event_name || '-').padEnd(14);
|
|
203
|
+
const namePainted = paintEvent(name, String(row.event_name || ''));
|
|
204
|
+
const who = formatActor(row);
|
|
205
|
+
const site = row.site_name || row.site_id ? `site=${row.site_name || row.site_id}` : '';
|
|
206
|
+
const tail = formatTail(row);
|
|
207
|
+
return `${(0, ui_1.dim)(ts)} ${namePainted} ${who}${site ? ' ' + (0, ui_1.dim)(site) : ''}${tail ? ' ' + tail : ''}\n`;
|
|
208
|
+
}
|
|
209
|
+
function formatTimestamp(ts) {
|
|
210
|
+
if (!ts)
|
|
211
|
+
return '----------------'.padEnd(19);
|
|
212
|
+
// ClickHouse format: '2026-04-27 14:23:01' (UTC) or ISO. Take the first 19.
|
|
213
|
+
const cleaned = ts.replace('T', ' ').slice(0, 19);
|
|
214
|
+
return cleaned.padEnd(19);
|
|
215
|
+
}
|
|
216
|
+
function paintEvent(padded, raw) {
|
|
217
|
+
if (!raw)
|
|
218
|
+
return padded;
|
|
219
|
+
if (raw.startsWith('$'))
|
|
220
|
+
return (0, ui_1.cyan)(padded); // system events
|
|
221
|
+
if (raw.startsWith('error'))
|
|
222
|
+
return (0, ui_1.yellow)(padded);
|
|
223
|
+
if (raw === 'page_view' || raw === 'pageview')
|
|
224
|
+
return (0, ui_1.dim)(padded);
|
|
225
|
+
return (0, ui_1.green)(padded);
|
|
226
|
+
}
|
|
227
|
+
function formatActor(row) {
|
|
228
|
+
if (row.user_id)
|
|
229
|
+
return `${(0, ui_1.bold)('user')}=${String(row.user_id).slice(0, 8)}`;
|
|
230
|
+
if (row.anonymous_id)
|
|
231
|
+
return `anon=${String(row.anonymous_id).slice(0, 6)}`;
|
|
232
|
+
return (0, ui_1.dim)('anon=?');
|
|
233
|
+
}
|
|
234
|
+
function formatTail(row) {
|
|
235
|
+
const parts = [];
|
|
236
|
+
const url = row.url || row.page_url;
|
|
237
|
+
if (url)
|
|
238
|
+
parts.push(stripHost(String(url)));
|
|
239
|
+
if (row.selector)
|
|
240
|
+
parts.push(`selector=${row.selector}`);
|
|
241
|
+
if (row.revenue_value != null && Number(row.revenue_value) > 0) {
|
|
242
|
+
const cur = row.revenue_currency || 'USD';
|
|
243
|
+
parts.push(`value=${row.revenue_value} ${cur}`);
|
|
244
|
+
}
|
|
245
|
+
return parts.join(' ');
|
|
246
|
+
}
|
|
247
|
+
function stripHost(url) {
|
|
248
|
+
try {
|
|
249
|
+
const u = new URL(url);
|
|
250
|
+
return u.pathname + (u.search || '');
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return url;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function sleep(ms) {
|
|
257
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
258
|
+
}
|
|
@@ -11,25 +11,47 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
function detectFramework(projectDir) {
|
|
12
12
|
const pkgPath = path_1.default.join(projectDir, 'package.json');
|
|
13
13
|
const hasPkgJson = fs_1.default.existsSync(pkgPath);
|
|
14
|
+
// Read package.json early so we can dedupe iOS-vs-React-Native (RN repos
|
|
15
|
+
// commonly host an `ios/MyApp.xcodeproj` and a top-level Package.swift in
|
|
16
|
+
// monorepos — without the RN check first we'd return `ios-swift` and ship
|
|
17
|
+
// the wrong SDK snippet).
|
|
18
|
+
let deps = {};
|
|
19
|
+
if (hasPkgJson) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
22
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* malformed package.json — fall through to filesystem detection */
|
|
26
|
+
}
|
|
27
|
+
if (deps['react-native'])
|
|
28
|
+
return 'react-native';
|
|
29
|
+
}
|
|
14
30
|
// Mobile framework detection (non-package.json based)
|
|
15
31
|
if (fs_1.default.existsSync(path_1.default.join(projectDir, 'pubspec.yaml')))
|
|
16
32
|
return 'flutter';
|
|
17
33
|
const hasSwiftPkg = fs_1.default.existsSync(path_1.default.join(projectDir, 'Package.swift'));
|
|
18
|
-
|
|
34
|
+
let hasXcodeProj = false;
|
|
35
|
+
try {
|
|
36
|
+
hasXcodeProj = fs_1.default
|
|
37
|
+
.readdirSync(projectDir)
|
|
38
|
+
.some((f) => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* unreadable dir — treat as no Xcode project */
|
|
42
|
+
}
|
|
19
43
|
if (hasSwiftPkg || hasXcodeProj)
|
|
20
44
|
return 'ios-swift';
|
|
21
45
|
if (!hasPkgJson) {
|
|
22
46
|
// Android detection (no package.json means no RN false positive)
|
|
23
|
-
if (fs_1.default.existsSync(path_1.default.join(projectDir, 'build.gradle.kts')) ||
|
|
47
|
+
if (fs_1.default.existsSync(path_1.default.join(projectDir, 'build.gradle.kts')) ||
|
|
48
|
+
fs_1.default.existsSync(path_1.default.join(projectDir, 'build.gradle')) ||
|
|
49
|
+
fs_1.default.existsSync(path_1.default.join(projectDir, 'app', 'build.gradle')) ||
|
|
50
|
+
fs_1.default.existsSync(path_1.default.join(projectDir, 'app', 'build.gradle.kts'))) {
|
|
24
51
|
return 'android-kotlin';
|
|
25
52
|
}
|
|
26
53
|
return 'html';
|
|
27
54
|
}
|
|
28
|
-
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
29
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
30
|
-
// React Native (must check before other React frameworks)
|
|
31
|
-
if (deps['react-native'])
|
|
32
|
-
return 'react-native';
|
|
33
55
|
if (deps['next']) {
|
|
34
56
|
// Check for app router
|
|
35
57
|
if (fs_1.default.existsSync(path_1.default.join(projectDir, 'src', 'app')) || fs_1.default.existsSync(path_1.default.join(projectDir, 'app'))) {
|