@buoy-gg/events 2.1.1 โ 2.1.3
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/lib/commonjs/components/EventsCopySettingsView.js +39 -8
- package/lib/commonjs/components/EventsModal.js +4 -7
- package/lib/commonjs/components/UnifiedEventDetail.js +32 -1
- package/lib/commonjs/components/UnifiedEventFilters.js +50 -21
- package/lib/commonjs/components/UnifiedEventItem.js +6 -1
- package/lib/commonjs/hooks/useUnifiedEvents.js +137 -28
- package/lib/commonjs/index.js +6 -0
- package/lib/commonjs/stores/unifiedEventStore.js +61 -16
- package/lib/commonjs/types/copySettings.js +29 -0
- package/lib/commonjs/utils/autoDiscoverEventSources.js +261 -39
- package/lib/commonjs/utils/badgeSelectionStorage.js +32 -0
- package/lib/commonjs/utils/eventExportFormatter.js +778 -1
- package/lib/module/components/EventsCopySettingsView.js +40 -9
- package/lib/module/components/EventsModal.js +5 -8
- package/lib/module/components/UnifiedEventDetail.js +32 -1
- package/lib/module/components/UnifiedEventFilters.js +50 -21
- package/lib/module/components/UnifiedEventItem.js +6 -1
- package/lib/module/hooks/useUnifiedEvents.js +140 -31
- package/lib/module/index.js +4 -1
- package/lib/module/stores/unifiedEventStore.js +58 -16
- package/lib/module/types/copySettings.js +29 -0
- package/lib/module/utils/autoDiscoverEventSources.js +260 -39
- package/lib/module/utils/badgeSelectionStorage.js +30 -0
- package/lib/module/utils/eventExportFormatter.js +777 -1
- package/lib/typescript/components/UnifiedEventFilters.d.ts +3 -0
- package/lib/typescript/hooks/useUnifiedEvents.d.ts +2 -0
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/stores/unifiedEventStore.d.ts +18 -2
- package/lib/typescript/types/copySettings.d.ts +25 -1
- package/lib/typescript/types/index.d.ts +3 -1
- package/lib/typescript/utils/autoDiscoverEventSources.d.ts +17 -0
- package/lib/typescript/utils/badgeSelectionStorage.d.ts +9 -0
- package/lib/typescript/utils/eventExportFormatter.d.ts +4 -0
- package/package.json +3 -3
- package/src/components/EventsCopySettingsView.tsx +41 -5
- package/src/components/EventsModal.tsx +6 -17
- package/src/components/UnifiedEventDetail.tsx +28 -0
- package/src/components/UnifiedEventFilters.tsx +88 -21
- package/src/components/UnifiedEventItem.tsx +5 -0
- package/src/hooks/useUnifiedEvents.ts +153 -25
- package/src/index.tsx +4 -0
- package/src/stores/unifiedEventStore.ts +58 -12
- package/src/types/copySettings.ts +31 -1
- package/src/types/index.ts +4 -1
- package/src/utils/autoDiscoverEventSources.ts +268 -44
- package/src/utils/badgeSelectionStorage.ts +30 -0
- package/src/utils/eventExportFormatter.ts +797 -0
|
@@ -8,6 +8,7 @@ exports.filterEvents = filterEvents;
|
|
|
8
8
|
exports.generateExport = generateExport;
|
|
9
9
|
exports.generateJsonExport = generateJsonExport;
|
|
10
10
|
exports.generateMarkdownExport = generateMarkdownExport;
|
|
11
|
+
exports.generateMermaidExport = generateMermaidExport;
|
|
11
12
|
exports.generatePlaintextExport = generatePlaintextExport;
|
|
12
13
|
exports.getExportSummary = getExportSummary;
|
|
13
14
|
/**
|
|
@@ -55,7 +56,8 @@ const SOURCE_LABELS = {
|
|
|
55
56
|
"react-query": "Query",
|
|
56
57
|
"react-query-query": "Query",
|
|
57
58
|
"react-query-mutation": "Mutation",
|
|
58
|
-
route: "Route"
|
|
59
|
+
route: "Route",
|
|
60
|
+
render: "Render"
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
// ============================================================================
|
|
@@ -1077,6 +1079,779 @@ function generatePlaintextExport(events, settings) {
|
|
|
1077
1079
|
return lines.join("\n");
|
|
1078
1080
|
}
|
|
1079
1081
|
|
|
1082
|
+
// ============================================================================
|
|
1083
|
+
// MERMAID SEQUENCE DIAGRAM EXPORT
|
|
1084
|
+
// ============================================================================
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Participant configuration for Mermaid sequence diagrams
|
|
1088
|
+
*/
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Participant definitions - map event sources to diagram actors
|
|
1092
|
+
* Organized by client vs server for grouping
|
|
1093
|
+
* Note: Storage types are split for clarity
|
|
1094
|
+
*/
|
|
1095
|
+
const MERMAID_PARTICIPANTS = [
|
|
1096
|
+
// Client-side
|
|
1097
|
+
{
|
|
1098
|
+
id: "U",
|
|
1099
|
+
icon: "๐ค",
|
|
1100
|
+
label: "User",
|
|
1101
|
+
sources: [],
|
|
1102
|
+
group: "client"
|
|
1103
|
+
}, {
|
|
1104
|
+
id: "App",
|
|
1105
|
+
icon: "๐ฑ",
|
|
1106
|
+
label: "App",
|
|
1107
|
+
sources: ["route", "render"],
|
|
1108
|
+
group: "client"
|
|
1109
|
+
}, {
|
|
1110
|
+
id: "Async",
|
|
1111
|
+
icon: "๐พ",
|
|
1112
|
+
label: "AsyncStorage",
|
|
1113
|
+
sources: ["storage-async"],
|
|
1114
|
+
group: "client"
|
|
1115
|
+
}, {
|
|
1116
|
+
id: "MMKV",
|
|
1117
|
+
icon: "โก",
|
|
1118
|
+
label: "MMKV",
|
|
1119
|
+
sources: ["storage-mmkv"],
|
|
1120
|
+
group: "client"
|
|
1121
|
+
}, {
|
|
1122
|
+
id: "Redux",
|
|
1123
|
+
icon: "๐",
|
|
1124
|
+
label: "Redux",
|
|
1125
|
+
sources: ["redux"],
|
|
1126
|
+
group: "client"
|
|
1127
|
+
},
|
|
1128
|
+
// Server-side / External
|
|
1129
|
+
{
|
|
1130
|
+
id: "API",
|
|
1131
|
+
icon: "๐",
|
|
1132
|
+
label: "API",
|
|
1133
|
+
sources: ["network"],
|
|
1134
|
+
group: "server"
|
|
1135
|
+
}, {
|
|
1136
|
+
id: "Cache",
|
|
1137
|
+
icon: "๐ฆ",
|
|
1138
|
+
label: "QueryCache",
|
|
1139
|
+
sources: ["react-query", "react-query-query", "react-query-mutation"],
|
|
1140
|
+
group: "server"
|
|
1141
|
+
}];
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Get the Mermaid participant ID for an event source
|
|
1145
|
+
*/
|
|
1146
|
+
function getParticipantId(source) {
|
|
1147
|
+
const participant = MERMAID_PARTICIPANTS.find(p => p.sources.includes(source));
|
|
1148
|
+
return participant?.id || "App";
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Detect which participants are needed based on events
|
|
1153
|
+
*/
|
|
1154
|
+
function detectParticipants(events) {
|
|
1155
|
+
const sourcesInUse = new Set();
|
|
1156
|
+
for (const event of events) {
|
|
1157
|
+
sourcesInUse.add(event.source);
|
|
1158
|
+
}
|
|
1159
|
+
const participants = [];
|
|
1160
|
+
|
|
1161
|
+
// Always include User and App
|
|
1162
|
+
participants.push(MERMAID_PARTICIPANTS[0]); // User
|
|
1163
|
+
participants.push(MERMAID_PARTICIPANTS[1]); // App
|
|
1164
|
+
|
|
1165
|
+
// Add other participants based on event sources present
|
|
1166
|
+
for (const participant of MERMAID_PARTICIPANTS.slice(2)) {
|
|
1167
|
+
if (participant.sources.some(s => sourcesInUse.has(s))) {
|
|
1168
|
+
participants.push(participant);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return participants;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Check if we should use participant grouping (boxes)
|
|
1176
|
+
*/
|
|
1177
|
+
function shouldUseGroups(participants) {
|
|
1178
|
+
const hasClient = participants.some(p => p.group === "client");
|
|
1179
|
+
const hasServer = participants.some(p => p.group === "server");
|
|
1180
|
+
return hasClient && hasServer && participants.length >= 3;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Format duration for Mermaid notes
|
|
1185
|
+
*/
|
|
1186
|
+
function formatMermaidDuration(ms) {
|
|
1187
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1188
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
1189
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Escape text for Mermaid (handle special characters)
|
|
1194
|
+
*/
|
|
1195
|
+
function escapeMermaidText(text) {
|
|
1196
|
+
return text.replace(/[#;:]/g, " ") // Remove characters with special meaning
|
|
1197
|
+
.replace(/[\n\r]/g, " ") // Replace newlines with spaces
|
|
1198
|
+
.replace(/"/g, "'") // Replace double quotes with single
|
|
1199
|
+
.replace(/\s+/g, " ") // Collapse multiple spaces
|
|
1200
|
+
.trim();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Check if a storage key is internal/devtools tracking
|
|
1205
|
+
* Be conservative - only filter obvious internal keys
|
|
1206
|
+
*/
|
|
1207
|
+
function isInternalStorageKey(key) {
|
|
1208
|
+
const internalPatterns = ["last-route", "@dev/last", "__debug", "_persist", "devtools/internal"];
|
|
1209
|
+
const lowerKey = key.toLowerCase();
|
|
1210
|
+
return internalPatterns.some(p => lowerKey.includes(p));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Format bytes to human readable size
|
|
1215
|
+
*/
|
|
1216
|
+
function formatBytes(bytes) {
|
|
1217
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1218
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1219
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Estimate size of data in bytes
|
|
1224
|
+
*/
|
|
1225
|
+
function estimateDataSize(data) {
|
|
1226
|
+
try {
|
|
1227
|
+
return JSON.stringify(data).length;
|
|
1228
|
+
} catch {
|
|
1229
|
+
return 0;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Get canonical URL for duplicate detection (strip timestamps, debug params)
|
|
1235
|
+
*/
|
|
1236
|
+
function getCanonicalUrl(url) {
|
|
1237
|
+
try {
|
|
1238
|
+
const urlObj = new URL(url, "http://localhost");
|
|
1239
|
+
// Remove common cache-busting/debug params
|
|
1240
|
+
const paramsToRemove = ["timestamp", "t", "_t", "debug", "client", "version", "cache", "_"];
|
|
1241
|
+
paramsToRemove.forEach(p => urlObj.searchParams.delete(p));
|
|
1242
|
+
return urlObj.pathname + (urlObj.searchParams.toString() ? "?" + urlObj.searchParams.toString() : "");
|
|
1243
|
+
} catch {
|
|
1244
|
+
return url;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Get a readable short name from a storage key
|
|
1250
|
+
*/
|
|
1251
|
+
function getStorageKeyName(key) {
|
|
1252
|
+
// Remove common prefixes
|
|
1253
|
+
let name = key.replace(/^@[\w-]+\//, "") // Remove @prefix/
|
|
1254
|
+
.replace(/^devtools\//, "").replace(/^dev\//, "").replace(/^buoy\//, "").replace(/^pokemon\//, ""); // Remove pokemon/ prefix for this app
|
|
1255
|
+
|
|
1256
|
+
// Get last meaningful segment
|
|
1257
|
+
const segments = name.split("/").filter(Boolean);
|
|
1258
|
+
if (segments.length > 1) {
|
|
1259
|
+
name = segments.slice(-1).join("/"); // Just the last segment
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Humanize common patterns
|
|
1263
|
+
name = name.replace(/-/g, " ").replace(/_/g, " ");
|
|
1264
|
+
|
|
1265
|
+
// Title case
|
|
1266
|
+
name = name.split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
1267
|
+
|
|
1268
|
+
// Common renames for clarity
|
|
1269
|
+
if (name.toLowerCase() === "saved") name = "Favorites";
|
|
1270
|
+
return name.substring(0, 20);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Extract entity name from URL path (e.g., /pokemon/charizard -> Charizard)
|
|
1275
|
+
*/
|
|
1276
|
+
function extractEntityFromPath(url) {
|
|
1277
|
+
// Strip query params first
|
|
1278
|
+
let path = url;
|
|
1279
|
+
try {
|
|
1280
|
+
const urlObj = new URL(url, "http://localhost");
|
|
1281
|
+
path = urlObj.pathname;
|
|
1282
|
+
} catch {
|
|
1283
|
+
// If URL parsing fails, try to strip query params manually
|
|
1284
|
+
const queryIndex = path.indexOf("?");
|
|
1285
|
+
if (queryIndex > 0) {
|
|
1286
|
+
path = path.substring(0, queryIndex);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Common patterns: /resource/:id, /resource/:name
|
|
1291
|
+
const segments = path.split("/").filter(Boolean);
|
|
1292
|
+
if (segments.length >= 2) {
|
|
1293
|
+
const lastSegment = segments[segments.length - 1];
|
|
1294
|
+
// If it looks like an ID (number or uuid), use the resource name
|
|
1295
|
+
if (/^\d+$/.test(lastSegment) || /^[a-f0-9-]{36}$/i.test(lastSegment)) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
// Humanize the last segment (e.g., charizard -> Charizard)
|
|
1299
|
+
return lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
|
|
1300
|
+
}
|
|
1301
|
+
if (segments.length === 1) {
|
|
1302
|
+
return segments[0].charAt(0).toUpperCase() + segments[0].slice(1);
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Get human-readable action from HTTP method + path
|
|
1309
|
+
*/
|
|
1310
|
+
function getHumanReadableApiAction(method, url) {
|
|
1311
|
+
try {
|
|
1312
|
+
const urlObj = new URL(url, "http://localhost");
|
|
1313
|
+
let path = urlObj.pathname;
|
|
1314
|
+
|
|
1315
|
+
// Handle GraphQL specially
|
|
1316
|
+
if (path.includes("graphql")) {
|
|
1317
|
+
return "GraphQL Request";
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Remove API prefixes
|
|
1321
|
+
path = path.replace(/^\/api\/v\d+/, "").replace(/^\/api/, "");
|
|
1322
|
+
const entity = extractEntityFromPath(path);
|
|
1323
|
+
const segments = path.split("/").filter(Boolean);
|
|
1324
|
+
const resource = segments[0] ? segments[0].charAt(0).toUpperCase() + segments[0].slice(1) : "Data";
|
|
1325
|
+
|
|
1326
|
+
// Check for search/query patterns
|
|
1327
|
+
const hasSearchQuery = urlObj.searchParams.has("q") || urlObj.searchParams.has("query") || urlObj.searchParams.has("search");
|
|
1328
|
+
|
|
1329
|
+
// Generate human-readable action based on method
|
|
1330
|
+
switch (method.toUpperCase()) {
|
|
1331
|
+
case "GET":
|
|
1332
|
+
if (hasSearchQuery) {
|
|
1333
|
+
const query = urlObj.searchParams.get("q") || urlObj.searchParams.get("query") || urlObj.searchParams.get("search");
|
|
1334
|
+
if (query) return `Search "${query}"`;
|
|
1335
|
+
return `Search ${resource}`;
|
|
1336
|
+
}
|
|
1337
|
+
if (entity) return `Fetch ${entity}`;
|
|
1338
|
+
return `Fetch ${resource}`;
|
|
1339
|
+
case "POST":
|
|
1340
|
+
if (path.includes("search")) return `Search ${resource}`;
|
|
1341
|
+
if (entity) return `Create ${entity}`;
|
|
1342
|
+
return `Create ${resource}`;
|
|
1343
|
+
case "PUT":
|
|
1344
|
+
case "PATCH":
|
|
1345
|
+
if (entity) return `Update ${entity}`;
|
|
1346
|
+
return `Update ${resource}`;
|
|
1347
|
+
case "DELETE":
|
|
1348
|
+
if (entity) return `Delete ${entity}`;
|
|
1349
|
+
return `Delete ${resource}`;
|
|
1350
|
+
default:
|
|
1351
|
+
return `${method} ${resource}`;
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
return `${method} request`;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Get human-readable route label
|
|
1360
|
+
*/
|
|
1361
|
+
function getHumanReadableRoute(pathname) {
|
|
1362
|
+
if (pathname === "/" || pathname === "") return "Home";
|
|
1363
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
1364
|
+
if (segments.length === 0) return "Home";
|
|
1365
|
+
|
|
1366
|
+
// Handle common patterns
|
|
1367
|
+
if (segments.length === 1) {
|
|
1368
|
+
// /pokemon -> Pokemon List
|
|
1369
|
+
const name = segments[0].charAt(0).toUpperCase() + segments[0].slice(1);
|
|
1370
|
+
return `${name} List`;
|
|
1371
|
+
}
|
|
1372
|
+
if (segments.length >= 2) {
|
|
1373
|
+
// /pokemon/charizard -> Charizard Details
|
|
1374
|
+
const entity = segments[segments.length - 1];
|
|
1375
|
+
// Check if it's a numeric ID
|
|
1376
|
+
if (/^\d+$/.test(entity)) {
|
|
1377
|
+
const resource = segments[0].charAt(0).toUpperCase() + segments[0].slice(1);
|
|
1378
|
+
return `${resource} #${entity}`;
|
|
1379
|
+
}
|
|
1380
|
+
const entityName = entity.charAt(0).toUpperCase() + entity.slice(1);
|
|
1381
|
+
return `${entityName} Details`;
|
|
1382
|
+
}
|
|
1383
|
+
return pathname;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Format a single event as Mermaid sequence messages
|
|
1388
|
+
* Returns both the messages and any notes to add
|
|
1389
|
+
* Returns null if the event should be skipped (internal/devtools)
|
|
1390
|
+
*/
|
|
1391
|
+
function formatMermaidEvent(event, _settings, baseTimestamp) {
|
|
1392
|
+
const messages = [];
|
|
1393
|
+
const notes = [];
|
|
1394
|
+
const relativeTime = formatMermaidDuration(event.timestamp - baseTimestamp);
|
|
1395
|
+
switch (event.source) {
|
|
1396
|
+
case "route":
|
|
1397
|
+
{
|
|
1398
|
+
const data = event.originalEvent;
|
|
1399
|
+
const pathname = String(data?.pathname || event.title);
|
|
1400
|
+
const previousPathname = data?.previousPathname;
|
|
1401
|
+
const timeSincePrevious = data?.timeSincePrevious;
|
|
1402
|
+
const routeLabel = getHumanReadableRoute(pathname);
|
|
1403
|
+
const fromLabel = previousPathname ? getHumanReadableRoute(previousPathname) : null;
|
|
1404
|
+
|
|
1405
|
+
// Show navigation with clear from โ to pattern
|
|
1406
|
+
if (fromLabel) {
|
|
1407
|
+
messages.push(`U->>App: ${fromLabel} โ ${routeLabel}`);
|
|
1408
|
+
// Show how long user spent on previous screen
|
|
1409
|
+
if (timeSincePrevious && timeSincePrevious > 0) {
|
|
1410
|
+
notes.push(`Note right of App: ${formatMermaidDuration(timeSincePrevious)} on ${fromLabel}`);
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
messages.push(`U->>App: Open ${routeLabel}`);
|
|
1414
|
+
}
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
case "network":
|
|
1418
|
+
{
|
|
1419
|
+
const data = event.originalEvent;
|
|
1420
|
+
const method = data?.method || "GET";
|
|
1421
|
+
const url = data?.url || "";
|
|
1422
|
+
const status = data?.status;
|
|
1423
|
+
const duration = data?.duration;
|
|
1424
|
+
const responseData = data?.response || data?.data || data?.body;
|
|
1425
|
+
// Try to get size from various possible fields
|
|
1426
|
+
const responseSize = data?.responseSize || data?.size || data?.contentLength;
|
|
1427
|
+
|
|
1428
|
+
// Get human-readable action
|
|
1429
|
+
const action = getHumanReadableApiAction(method, url);
|
|
1430
|
+
const entity = extractEntityFromPath(url);
|
|
1431
|
+
|
|
1432
|
+
// Request with activation
|
|
1433
|
+
messages.push(`App->>+API: ${action}`);
|
|
1434
|
+
|
|
1435
|
+
// Response with entity context if available
|
|
1436
|
+
if (event.status === "error") {
|
|
1437
|
+
const statusText = status ? `${status} Error` : "Failed";
|
|
1438
|
+
messages.push(`API--x-App: ${statusText}`);
|
|
1439
|
+
} else {
|
|
1440
|
+
let response = entity ? `โ ${entity}` : "โ Success";
|
|
1441
|
+
messages.push(`API-->>-App: ${response}`);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Build note with duration and size
|
|
1445
|
+
const noteParts = [];
|
|
1446
|
+
if (duration) {
|
|
1447
|
+
noteParts.push(formatMermaidDuration(duration));
|
|
1448
|
+
}
|
|
1449
|
+
// Add response size - try explicit size first, then estimate
|
|
1450
|
+
if (responseSize && responseSize > 0) {
|
|
1451
|
+
noteParts.push(formatBytes(responseSize));
|
|
1452
|
+
} else if (responseData) {
|
|
1453
|
+
const size = estimateDataSize(responseData);
|
|
1454
|
+
if (size > 1024) {
|
|
1455
|
+
// Only show if > 1KB
|
|
1456
|
+
noteParts.push(formatBytes(size));
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (noteParts.length > 0) {
|
|
1460
|
+
notes.push(`Note right of API: ${noteParts.join(" ยท ")}`);
|
|
1461
|
+
}
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
case "storage-async":
|
|
1465
|
+
case "storage-mmkv":
|
|
1466
|
+
{
|
|
1467
|
+
const data = event.originalEvent;
|
|
1468
|
+
const innerData = data?.data;
|
|
1469
|
+
const action = data?.action || "access";
|
|
1470
|
+
const key = innerData?.key || "data";
|
|
1471
|
+
|
|
1472
|
+
// Skip internal devtools storage
|
|
1473
|
+
if (isInternalStorageKey(key)) {
|
|
1474
|
+
return {
|
|
1475
|
+
messages: [],
|
|
1476
|
+
notes: [],
|
|
1477
|
+
skip: true
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const keyName = getStorageKeyName(key);
|
|
1481
|
+
const storeId = event.source === "storage-mmkv" ? "MMKV" : "Async";
|
|
1482
|
+
|
|
1483
|
+
// Helper to get item identifier (handles both objects and strings)
|
|
1484
|
+
const getItemId = item => {
|
|
1485
|
+
if (typeof item === "string") return item;
|
|
1486
|
+
if (typeof item === "number") return String(item);
|
|
1487
|
+
if (typeof item === "object" && item) {
|
|
1488
|
+
if ("name" in item) return String(item.name);
|
|
1489
|
+
if ("id" in item) return String(item.id);
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// Helper to capitalize first letter
|
|
1495
|
+
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
1496
|
+
|
|
1497
|
+
// Analyze the change to determine action and entity
|
|
1498
|
+
let actionIcon = "";
|
|
1499
|
+
let actionVerb = "";
|
|
1500
|
+
let entityName = "";
|
|
1501
|
+
let countInfo = "";
|
|
1502
|
+
const prevValue = innerData?.prevValue;
|
|
1503
|
+
const newValue = innerData?.value;
|
|
1504
|
+
|
|
1505
|
+
// Handle array comparisons (most common for collections)
|
|
1506
|
+
if (Array.isArray(prevValue) && Array.isArray(newValue)) {
|
|
1507
|
+
const prevLen = prevValue.length;
|
|
1508
|
+
const newLen = newValue.length;
|
|
1509
|
+
countInfo = `${prevLen} โ ${newLen} items`;
|
|
1510
|
+
if (newLen > prevLen) {
|
|
1511
|
+
// Items were added
|
|
1512
|
+
actionIcon = "โ";
|
|
1513
|
+
actionVerb = "Add";
|
|
1514
|
+
// Find what was added
|
|
1515
|
+
const prevSet = new Set(prevValue.map(getItemId).filter(Boolean));
|
|
1516
|
+
for (const item of newValue) {
|
|
1517
|
+
const id = getItemId(item);
|
|
1518
|
+
if (id && !prevSet.has(id)) {
|
|
1519
|
+
entityName = capitalize(id);
|
|
1520
|
+
break;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
} else if (newLen < prevLen) {
|
|
1524
|
+
// Items were removed
|
|
1525
|
+
actionIcon = "โ";
|
|
1526
|
+
actionVerb = "Remove";
|
|
1527
|
+
// Find what was removed
|
|
1528
|
+
const newSet = new Set(newValue.map(getItemId).filter(Boolean));
|
|
1529
|
+
for (const item of prevValue) {
|
|
1530
|
+
const id = getItemId(item);
|
|
1531
|
+
if (id && !newSet.has(id)) {
|
|
1532
|
+
entityName = capitalize(id);
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
} else {
|
|
1537
|
+
// Same length - update
|
|
1538
|
+
actionIcon = "๐";
|
|
1539
|
+
actionVerb = "Update";
|
|
1540
|
+
}
|
|
1541
|
+
} else if (Array.isArray(newValue) && (!prevValue || Array.isArray(prevValue) && prevValue.length === 0)) {
|
|
1542
|
+
// Adding first item(s) - prevValue is null/undefined or empty array
|
|
1543
|
+
actionIcon = "โ";
|
|
1544
|
+
actionVerb = "Add";
|
|
1545
|
+
countInfo = `โ ${newValue.length} items`;
|
|
1546
|
+
if (newValue.length > 0) {
|
|
1547
|
+
const id = getItemId(newValue[0]);
|
|
1548
|
+
if (id) entityName = capitalize(id);
|
|
1549
|
+
}
|
|
1550
|
+
} else if (Array.isArray(prevValue) && (!newValue || Array.isArray(newValue) && newValue.length === 0)) {
|
|
1551
|
+
// Removing all items
|
|
1552
|
+
actionIcon = "โ";
|
|
1553
|
+
actionVerb = "Clear";
|
|
1554
|
+
countInfo = `${prevValue.length} โ 0 items`;
|
|
1555
|
+
} else if (action === "getItem") {
|
|
1556
|
+
actionIcon = "๐";
|
|
1557
|
+
actionVerb = "Load";
|
|
1558
|
+
} else if (action === "removeItem") {
|
|
1559
|
+
actionIcon = "โ";
|
|
1560
|
+
actionVerb = "Remove";
|
|
1561
|
+
} else if (action === "clear") {
|
|
1562
|
+
actionIcon = "๐๏ธ";
|
|
1563
|
+
actionVerb = "Clear";
|
|
1564
|
+
} else {
|
|
1565
|
+
// Default for setItem without prev/new comparison
|
|
1566
|
+
actionIcon = "๐พ";
|
|
1567
|
+
actionVerb = "Save";
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Build the message
|
|
1571
|
+
let message;
|
|
1572
|
+
if (entityName) {
|
|
1573
|
+
message = `App->>${storeId}: ${actionIcon} ${actionVerb} ${entityName} โ ${keyName}`;
|
|
1574
|
+
} else {
|
|
1575
|
+
message = `App->>${storeId}: ${actionIcon} ${actionVerb} ${keyName}`;
|
|
1576
|
+
}
|
|
1577
|
+
messages.push(message);
|
|
1578
|
+
|
|
1579
|
+
// Add note with count info
|
|
1580
|
+
if (countInfo) {
|
|
1581
|
+
notes.push(`Note right of ${storeId}: ${countInfo}`);
|
|
1582
|
+
}
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
case "react-query":
|
|
1586
|
+
case "react-query-query":
|
|
1587
|
+
{
|
|
1588
|
+
const data = event.originalEvent;
|
|
1589
|
+
const type = data?.type || "";
|
|
1590
|
+
const queryKey = data?.queryKey;
|
|
1591
|
+
const duration = data?.duration;
|
|
1592
|
+
|
|
1593
|
+
// Format query key as human-readable
|
|
1594
|
+
let keyLabel = "data";
|
|
1595
|
+
if (Array.isArray(queryKey) && queryKey.length > 0) {
|
|
1596
|
+
// Try to make it readable: ["pokemon", "charizard"] -> "Charizard"
|
|
1597
|
+
const lastKey = queryKey[queryKey.length - 1];
|
|
1598
|
+
if (typeof lastKey === "string" && !/^\d+$/.test(lastKey)) {
|
|
1599
|
+
keyLabel = lastKey.charAt(0).toUpperCase() + lastKey.slice(1);
|
|
1600
|
+
} else if (queryKey.length > 1 && typeof queryKey[0] === "string") {
|
|
1601
|
+
keyLabel = queryKey[0].charAt(0).toUpperCase() + queryKey[0].slice(1);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (type.includes("start") || type.includes("fetch") || event.status === "pending") {
|
|
1605
|
+
messages.push(`App->>+Cache: Fetch ${keyLabel}`);
|
|
1606
|
+
} else if (event.status === "error") {
|
|
1607
|
+
messages.push(`Cache--x-App: Failed to load`);
|
|
1608
|
+
} else if (event.status === "success") {
|
|
1609
|
+
let response = `โ ${keyLabel} ready`;
|
|
1610
|
+
if (duration) {
|
|
1611
|
+
response += ` (${formatMermaidDuration(duration)})`;
|
|
1612
|
+
}
|
|
1613
|
+
messages.push(`Cache-->>-App: ${response}`);
|
|
1614
|
+
} else if (type.includes("invalidate")) {
|
|
1615
|
+
messages.push(`App->>Cache: Refresh ${keyLabel}`);
|
|
1616
|
+
} else {
|
|
1617
|
+
messages.push(`Cache-->>App: ${escapeMermaidText(type)}`);
|
|
1618
|
+
}
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
case "react-query-mutation":
|
|
1622
|
+
{
|
|
1623
|
+
const data = event.originalEvent;
|
|
1624
|
+
const type = data?.type || "";
|
|
1625
|
+
const mutationKey = data?.mutationKey;
|
|
1626
|
+
const duration = data?.duration;
|
|
1627
|
+
let actionLabel = "Update";
|
|
1628
|
+
if (Array.isArray(mutationKey) && mutationKey.length > 0) {
|
|
1629
|
+
const key = String(mutationKey[0]);
|
|
1630
|
+
// Common mutation patterns
|
|
1631
|
+
if (key.includes("add") || key.includes("create")) actionLabel = "Add";else if (key.includes("delete") || key.includes("remove")) actionLabel = "Delete";else if (key.includes("update") || key.includes("edit")) actionLabel = "Update";else actionLabel = key.charAt(0).toUpperCase() + key.slice(1);
|
|
1632
|
+
}
|
|
1633
|
+
if (type.includes("start") || type.includes("loading") || event.status === "pending") {
|
|
1634
|
+
messages.push(`App->>+Cache: ${actionLabel}`);
|
|
1635
|
+
} else if (event.status === "error") {
|
|
1636
|
+
messages.push(`Cache--x-App: ${actionLabel} failed`);
|
|
1637
|
+
} else if (event.status === "success") {
|
|
1638
|
+
let response = `โ ${actionLabel} complete`;
|
|
1639
|
+
if (duration) {
|
|
1640
|
+
response += ` (${formatMermaidDuration(duration)})`;
|
|
1641
|
+
}
|
|
1642
|
+
messages.push(`Cache-->>-App: ${response}`);
|
|
1643
|
+
}
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
case "redux":
|
|
1647
|
+
{
|
|
1648
|
+
const data = event.originalEvent;
|
|
1649
|
+
const actionType = data?.type || event.title;
|
|
1650
|
+
const hasChange = data?.hasStateChange;
|
|
1651
|
+
const diffSummary = data?.diffSummary;
|
|
1652
|
+
|
|
1653
|
+
// Parse action type into human-readable form
|
|
1654
|
+
// e.g., "cart/addItem" -> "Add Item to Cart"
|
|
1655
|
+
const parts = actionType.split("/");
|
|
1656
|
+
let actionLabel;
|
|
1657
|
+
if (parts.length >= 2) {
|
|
1658
|
+
const slice = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
1659
|
+
let action = parts[1].replace(/([A-Z])/g, " $1") // camelCase to spaces
|
|
1660
|
+
.replace(/(pending|fulfilled|rejected)$/i, "").trim();
|
|
1661
|
+
action = action.charAt(0).toUpperCase() + action.slice(1);
|
|
1662
|
+
actionLabel = `${action} (${slice})`;
|
|
1663
|
+
} else {
|
|
1664
|
+
actionLabel = escapeMermaidText(actionType).substring(0, 25);
|
|
1665
|
+
}
|
|
1666
|
+
messages.push(`App->>Redux: ${actionLabel}`);
|
|
1667
|
+
if (hasChange === false) {
|
|
1668
|
+
notes.push(`Note right of Redux: No change`);
|
|
1669
|
+
} else if (diffSummary) {
|
|
1670
|
+
notes.push(`Note right of Redux: ${escapeMermaidText(diffSummary).substring(0, 35)}`);
|
|
1671
|
+
}
|
|
1672
|
+
break;
|
|
1673
|
+
}
|
|
1674
|
+
case "render":
|
|
1675
|
+
{
|
|
1676
|
+
const data = event.originalEvent;
|
|
1677
|
+
const renderData = data?.renderData;
|
|
1678
|
+
const componentName = escapeMermaidText(event.title).substring(0, 20);
|
|
1679
|
+
const cause = renderData?.cause || "update";
|
|
1680
|
+
|
|
1681
|
+
// Make cause more human-readable
|
|
1682
|
+
const causeLabel = cause === "mount" ? "mounted" : cause === "props" ? "props changed" : cause === "state" ? "state changed" : cause === "context" ? "context changed" : cause === "parent" ? "parent re-rendered" : cause === "hooks" ? "hook changed" : cause;
|
|
1683
|
+
messages.push(`App->>App: Render ${componentName}`);
|
|
1684
|
+
notes.push(`Note right of App: ${causeLabel}`);
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
default:
|
|
1688
|
+
{
|
|
1689
|
+
messages.push(`App->>App: ${escapeMermaidText(event.title).substring(0, 30)}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
messages,
|
|
1694
|
+
notes
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Generate Mermaid sequence diagram export
|
|
1700
|
+
*/
|
|
1701
|
+
function generateMermaidExport(events, settings) {
|
|
1702
|
+
const filtered = filterEvents(events, settings);
|
|
1703
|
+
if (filtered.length === 0) {
|
|
1704
|
+
return "sequenceDiagram\n participant U as ๐ค User\n participant App as ๐ฑ App\n Note over U,App: No events captured";
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Sort by timestamp (oldest first for timeline)
|
|
1708
|
+
const sorted = [...filtered].sort((a, b) => a.timestamp - b.timestamp);
|
|
1709
|
+
const baseTimestamp = sorted[0].timestamp;
|
|
1710
|
+
|
|
1711
|
+
// Detect participants
|
|
1712
|
+
const participants = detectParticipants(sorted);
|
|
1713
|
+
const useGroups = shouldUseGroups(participants);
|
|
1714
|
+
const lines = ["sequenceDiagram"];
|
|
1715
|
+
lines.push(" autonumber");
|
|
1716
|
+
lines.push("");
|
|
1717
|
+
|
|
1718
|
+
// Add participants with optional grouping
|
|
1719
|
+
if (useGroups) {
|
|
1720
|
+
const clientParticipants = participants.filter(p => p.group === "client");
|
|
1721
|
+
const serverParticipants = participants.filter(p => p.group === "server");
|
|
1722
|
+
if (clientParticipants.length > 0) {
|
|
1723
|
+
lines.push(" box rgb(240, 248, 255) Client");
|
|
1724
|
+
for (const p of clientParticipants) {
|
|
1725
|
+
lines.push(` participant ${p.id} as ${p.icon} ${p.label}`);
|
|
1726
|
+
}
|
|
1727
|
+
lines.push(" end");
|
|
1728
|
+
}
|
|
1729
|
+
if (serverParticipants.length > 0) {
|
|
1730
|
+
lines.push(" box rgb(255, 248, 240) External");
|
|
1731
|
+
for (const p of serverParticipants) {
|
|
1732
|
+
lines.push(` participant ${p.id} as ${p.icon} ${p.label}`);
|
|
1733
|
+
}
|
|
1734
|
+
lines.push(" end");
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
for (const p of participants) {
|
|
1738
|
+
lines.push(` participant ${p.id} as ${p.icon} ${p.label}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
lines.push("");
|
|
1742
|
+
|
|
1743
|
+
// Calculate session summary stats
|
|
1744
|
+
const sessionDuration = sorted[sorted.length - 1].timestamp - baseTimestamp;
|
|
1745
|
+
const networkEvents = sorted.filter(e => e.source === "network");
|
|
1746
|
+
const totalBytes = networkEvents.reduce((sum, e) => {
|
|
1747
|
+
const data = e.originalEvent;
|
|
1748
|
+
const size = data?.responseSize || data?.size || 0;
|
|
1749
|
+
return sum + size;
|
|
1750
|
+
}, 0);
|
|
1751
|
+
const errorCount = sorted.filter(e => e.status === "error").length;
|
|
1752
|
+
|
|
1753
|
+
// Build summary parts
|
|
1754
|
+
const summaryParts = [];
|
|
1755
|
+
summaryParts.push(`โฑ๏ธ ${formatMermaidDuration(sessionDuration)}`);
|
|
1756
|
+
summaryParts.push(`๐ ${sorted.length} events`);
|
|
1757
|
+
if (networkEvents.length > 0) {
|
|
1758
|
+
summaryParts.push(`๐ ${networkEvents.length} API calls`);
|
|
1759
|
+
}
|
|
1760
|
+
if (totalBytes > 0) {
|
|
1761
|
+
summaryParts.push(`๐ฆ ${formatBytes(totalBytes)}`);
|
|
1762
|
+
}
|
|
1763
|
+
if (errorCount > 0) {
|
|
1764
|
+
summaryParts.push(`โ ${errorCount} errors`);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Add session summary header
|
|
1768
|
+
lines.push(` Note over U,API: ${summaryParts.join(" ยท ")}`);
|
|
1769
|
+
lines.push("");
|
|
1770
|
+
|
|
1771
|
+
// Track for duplicate detection - use canonical URLs (without cache-busting params)
|
|
1772
|
+
const recentNetworkUrls = new Map();
|
|
1773
|
+
|
|
1774
|
+
// Process events
|
|
1775
|
+
let skippedCount = 0;
|
|
1776
|
+
for (const event of sorted) {
|
|
1777
|
+
const result = formatMermaidEvent(event, settings, baseTimestamp);
|
|
1778
|
+
|
|
1779
|
+
// Skip internal/devtools events
|
|
1780
|
+
if (result.skip) {
|
|
1781
|
+
skippedCount++;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const {
|
|
1785
|
+
messages,
|
|
1786
|
+
notes
|
|
1787
|
+
} = result;
|
|
1788
|
+
|
|
1789
|
+
// Check for duplicate network requests using canonical URL
|
|
1790
|
+
let isDuplicate = false;
|
|
1791
|
+
let duplicateInfo = "";
|
|
1792
|
+
if (event.source === "network") {
|
|
1793
|
+
const data = event.originalEvent;
|
|
1794
|
+
const url = data?.url;
|
|
1795
|
+
if (url) {
|
|
1796
|
+
const canonicalUrl = getCanonicalUrl(url);
|
|
1797
|
+
const existing = recentNetworkUrls.get(canonicalUrl);
|
|
1798
|
+
if (existing && event.timestamp - existing.timestamp < 30000) {
|
|
1799
|
+
// 30 second window
|
|
1800
|
+
isDuplicate = true;
|
|
1801
|
+
const timeSince = event.timestamp - existing.timestamp;
|
|
1802
|
+
const timeLabel = formatMermaidDuration(timeSince);
|
|
1803
|
+
duplicateInfo = `โ ๏ธ Already fetched ${timeLabel} ago`;
|
|
1804
|
+
recentNetworkUrls.set(canonicalUrl, {
|
|
1805
|
+
timestamp: event.timestamp,
|
|
1806
|
+
count: existing.count + 1
|
|
1807
|
+
});
|
|
1808
|
+
} else {
|
|
1809
|
+
recentNetworkUrls.set(canonicalUrl, {
|
|
1810
|
+
timestamp: event.timestamp,
|
|
1811
|
+
count: 1
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Wrap duplicates or errors in highlighted rect
|
|
1818
|
+
if (isDuplicate) {
|
|
1819
|
+
lines.push(" rect rgb(255, 245, 230)");
|
|
1820
|
+
lines.push(` Note over App,API: ${duplicateInfo}`);
|
|
1821
|
+
for (const msg of messages) {
|
|
1822
|
+
lines.push(` ${msg}`);
|
|
1823
|
+
}
|
|
1824
|
+
for (const note of notes) {
|
|
1825
|
+
lines.push(` ${note}`);
|
|
1826
|
+
}
|
|
1827
|
+
lines.push(" end");
|
|
1828
|
+
} else if (event.status === "error") {
|
|
1829
|
+
lines.push(" rect rgb(255, 235, 235)");
|
|
1830
|
+
for (const msg of messages) {
|
|
1831
|
+
lines.push(` ${msg}`);
|
|
1832
|
+
}
|
|
1833
|
+
for (const note of notes) {
|
|
1834
|
+
lines.push(` ${note}`);
|
|
1835
|
+
}
|
|
1836
|
+
lines.push(" end");
|
|
1837
|
+
} else {
|
|
1838
|
+
for (const msg of messages) {
|
|
1839
|
+
lines.push(` ${msg}`);
|
|
1840
|
+
}
|
|
1841
|
+
for (const note of notes) {
|
|
1842
|
+
lines.push(` ${note}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Add note about skipped internal events if any
|
|
1848
|
+
if (skippedCount > 0) {
|
|
1849
|
+
lines.push("");
|
|
1850
|
+
lines.push(` Note over App: ${skippedCount} internal events hidden`);
|
|
1851
|
+
}
|
|
1852
|
+
return lines.join("\n");
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1080
1855
|
/**
|
|
1081
1856
|
* Generate export based on settings format
|
|
1082
1857
|
*/
|
|
@@ -1088,6 +1863,8 @@ function generateExport(events, settings) {
|
|
|
1088
1863
|
return generateJsonExport(events, settings);
|
|
1089
1864
|
case "plaintext":
|
|
1090
1865
|
return generatePlaintextExport(events, settings);
|
|
1866
|
+
case "mermaid":
|
|
1867
|
+
return generateMermaidExport(events, settings);
|
|
1091
1868
|
default:
|
|
1092
1869
|
return generateMarkdownExport(events, settings);
|
|
1093
1870
|
}
|