@getlimelight/sdk 0.1.5 → 0.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/dist/index.d.mts +84 -3
- package/dist/index.d.ts +84 -3
- package/dist/index.js +617 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +617 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -255,9 +255,20 @@ var SENSITIVE_HEADERS = [
|
|
|
255
255
|
"x-secret",
|
|
256
256
|
"bearer"
|
|
257
257
|
];
|
|
258
|
-
var
|
|
259
|
-
var
|
|
260
|
-
|
|
258
|
+
var SDK_VERSION = true ? "0.2.1" : "test-version";
|
|
259
|
+
var RENDER_THRESHOLDS = {
|
|
260
|
+
HOT_VELOCITY: 5,
|
|
261
|
+
HIGH_RENDER_COUNT: 50,
|
|
262
|
+
VELOCITY_WINDOW_MS: 2e3,
|
|
263
|
+
SNAPSHOT_INTERVAL_MS: 1e3,
|
|
264
|
+
MIN_DELTA_TO_EMIT: 1,
|
|
265
|
+
MAX_PROP_KEYS_TO_TRACK: 20,
|
|
266
|
+
// Don't track more than this many unique props
|
|
267
|
+
MAX_PROP_CHANGES_PER_SNAPSHOT: 10,
|
|
268
|
+
// Limit delta array size
|
|
269
|
+
TOP_PROPS_TO_REPORT: 5
|
|
270
|
+
// Only report top N changed props
|
|
271
|
+
};
|
|
261
272
|
|
|
262
273
|
// src/helpers/safety/redactSensitiveHeaders.ts
|
|
263
274
|
var redactSensitiveHeaders = (headers) => {
|
|
@@ -437,6 +448,42 @@ var formatRequestName = (url) => {
|
|
|
437
448
|
}
|
|
438
449
|
};
|
|
439
450
|
|
|
451
|
+
// src/helpers/render/generateRenderId.ts
|
|
452
|
+
var counter = 0;
|
|
453
|
+
var generateRenderId = () => {
|
|
454
|
+
const timestamp = Date.now().toString(36);
|
|
455
|
+
const count = (counter++).toString(36);
|
|
456
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
457
|
+
if (counter > 1e3) counter = 0;
|
|
458
|
+
return `${timestamp}${count}-${random}`;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/helpers/render/createEmptyCauseBreakdown.ts
|
|
462
|
+
var createEmptyCauseBreakdown = () => {
|
|
463
|
+
return {
|
|
464
|
+
["state_change" /* STATE_CHANGE */]: 0,
|
|
465
|
+
["props_change" /* PROPS_CHANGE */]: 0,
|
|
466
|
+
["context_change" /* CONTEXT_CHANGE */]: 0,
|
|
467
|
+
["parent_render" /* PARENT_RENDER */]: 0,
|
|
468
|
+
["force_update" /* FORCE_UPDATE */]: 0,
|
|
469
|
+
["unknown" /* UNKNOWN */]: 0
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// src/helpers/render/createEmptyPropChangeStats.ts
|
|
474
|
+
var createEmptyPropChangeStats = () => {
|
|
475
|
+
return {
|
|
476
|
+
changeCount: /* @__PURE__ */ new Map(),
|
|
477
|
+
referenceOnlyCount: /* @__PURE__ */ new Map()
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/helpers/render/getCurrentTransactionId.ts
|
|
482
|
+
var globalGetTransactionId = null;
|
|
483
|
+
var getCurrentTransactionId = () => {
|
|
484
|
+
return globalGetTransactionId?.() ?? null;
|
|
485
|
+
};
|
|
486
|
+
|
|
440
487
|
// src/limelight/interceptors/ConsoleInterceptor.ts
|
|
441
488
|
var ConsoleInterceptor = class {
|
|
442
489
|
constructor(sendMessage, getSessionId) {
|
|
@@ -989,6 +1036,561 @@ var XHRInterceptor = class {
|
|
|
989
1036
|
}
|
|
990
1037
|
};
|
|
991
1038
|
|
|
1039
|
+
// src/limelight/interceptors/RenderInterceptor.ts
|
|
1040
|
+
var RenderInterceptor = class {
|
|
1041
|
+
sendMessage;
|
|
1042
|
+
getSessionId;
|
|
1043
|
+
config = null;
|
|
1044
|
+
isSetup = false;
|
|
1045
|
+
profiles = /* @__PURE__ */ new Map();
|
|
1046
|
+
fiberToComponentId = /* @__PURE__ */ new WeakMap();
|
|
1047
|
+
componentIdCounter = 0;
|
|
1048
|
+
snapshotTimer = null;
|
|
1049
|
+
currentCommitComponents = /* @__PURE__ */ new Set();
|
|
1050
|
+
componentsInCurrentCommit = 0;
|
|
1051
|
+
originalHook = null;
|
|
1052
|
+
originalOnCommitFiberRoot = null;
|
|
1053
|
+
originalOnCommitFiberUnmount = null;
|
|
1054
|
+
pendingUnmounts = [];
|
|
1055
|
+
constructor(sendMessage, getSessionId) {
|
|
1056
|
+
this.sendMessage = sendMessage;
|
|
1057
|
+
this.getSessionId = getSessionId;
|
|
1058
|
+
}
|
|
1059
|
+
setup(config) {
|
|
1060
|
+
if (this.isSetup) {
|
|
1061
|
+
console.warn("[Limelight] Render interceptor already set up");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
this.config = config;
|
|
1065
|
+
if (!this.installHook()) {
|
|
1066
|
+
console.warn("[Limelight] Failed to install render hook");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
this.snapshotTimer = setInterval(() => {
|
|
1070
|
+
this.emitSnapshot();
|
|
1071
|
+
}, RENDER_THRESHOLDS.SNAPSHOT_INTERVAL_MS);
|
|
1072
|
+
this.isSetup = true;
|
|
1073
|
+
}
|
|
1074
|
+
installHook() {
|
|
1075
|
+
const globalObj = typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : null;
|
|
1076
|
+
if (!globalObj) return false;
|
|
1077
|
+
const hookKey = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
1078
|
+
const existingHook = globalObj[hookKey];
|
|
1079
|
+
if (existingHook) {
|
|
1080
|
+
this.wrapExistingHook(existingHook);
|
|
1081
|
+
} else {
|
|
1082
|
+
this.createHook(globalObj, hookKey);
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
wrapExistingHook(hook) {
|
|
1087
|
+
this.originalHook = hook;
|
|
1088
|
+
this.originalOnCommitFiberRoot = hook.onCommitFiberRoot?.bind(hook);
|
|
1089
|
+
this.originalOnCommitFiberUnmount = hook.onCommitFiberUnmount?.bind(hook);
|
|
1090
|
+
hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
|
|
1091
|
+
this.originalOnCommitFiberRoot?.(rendererID, root, priorityLevel);
|
|
1092
|
+
this.handleCommitFiberRoot(rendererID, root);
|
|
1093
|
+
};
|
|
1094
|
+
hook.onCommitFiberUnmount = (rendererID, fiber) => {
|
|
1095
|
+
this.originalOnCommitFiberUnmount?.(rendererID, fiber);
|
|
1096
|
+
this.handleCommitFiberUnmount(rendererID, fiber);
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
createHook(globalObj, hookKey) {
|
|
1100
|
+
const renderers = /* @__PURE__ */ new Map();
|
|
1101
|
+
let rendererIdCounter = 0;
|
|
1102
|
+
const hook = {
|
|
1103
|
+
supportsFiber: true,
|
|
1104
|
+
inject: (renderer) => {
|
|
1105
|
+
const id = ++rendererIdCounter;
|
|
1106
|
+
renderers.set(id, renderer);
|
|
1107
|
+
return id;
|
|
1108
|
+
},
|
|
1109
|
+
onCommitFiberRoot: (rendererID, root, priorityLevel) => {
|
|
1110
|
+
this.handleCommitFiberRoot(rendererID, root);
|
|
1111
|
+
},
|
|
1112
|
+
onCommitFiberUnmount: (rendererID, fiber) => {
|
|
1113
|
+
this.handleCommitFiberUnmount(rendererID, fiber);
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
globalObj[hookKey] = hook;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Handles a fiber root commit - walks tree and ACCUMULATES into profiles.
|
|
1120
|
+
* Two-pass: first count components, then accumulate with distributed cost.
|
|
1121
|
+
*/
|
|
1122
|
+
handleCommitFiberRoot(_rendererID, root) {
|
|
1123
|
+
this.currentCommitComponents.clear();
|
|
1124
|
+
this.componentsInCurrentCommit = 0;
|
|
1125
|
+
try {
|
|
1126
|
+
this.countRenderedComponents(root.current);
|
|
1127
|
+
this.walkFiberTree(root.current, null, 0);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (isDevelopment()) {
|
|
1130
|
+
console.warn("[Limelight] Error processing fiber tree:", error);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* First pass: count rendered components for cost distribution.
|
|
1136
|
+
*/
|
|
1137
|
+
countRenderedComponents(fiber) {
|
|
1138
|
+
if (!fiber) return;
|
|
1139
|
+
if (this.isUserComponent(fiber) && this.didFiberRender(fiber)) {
|
|
1140
|
+
this.componentsInCurrentCommit++;
|
|
1141
|
+
}
|
|
1142
|
+
this.countRenderedComponents(fiber.child);
|
|
1143
|
+
this.countRenderedComponents(fiber.sibling);
|
|
1144
|
+
}
|
|
1145
|
+
handleCommitFiberUnmount(_rendererID, fiber) {
|
|
1146
|
+
if (!this.isUserComponent(fiber)) return;
|
|
1147
|
+
const componentId = this.fiberToComponentId.get(fiber);
|
|
1148
|
+
if (!componentId) return;
|
|
1149
|
+
const profile = this.profiles.get(componentId);
|
|
1150
|
+
if (profile) {
|
|
1151
|
+
profile.unmountedAt = Date.now();
|
|
1152
|
+
this.pendingUnmounts.push(profile);
|
|
1153
|
+
this.profiles.delete(componentId);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Walks fiber tree and accumulates render stats into profiles.
|
|
1158
|
+
*/
|
|
1159
|
+
walkFiberTree(fiber, parentComponentId, depth) {
|
|
1160
|
+
if (!fiber) return;
|
|
1161
|
+
if (this.isUserComponent(fiber) && this.didFiberRender(fiber)) {
|
|
1162
|
+
const componentId = this.getOrCreateComponentId(fiber);
|
|
1163
|
+
this.accumulateRender(fiber, componentId, parentComponentId, depth);
|
|
1164
|
+
this.currentCommitComponents.add(componentId);
|
|
1165
|
+
parentComponentId = componentId;
|
|
1166
|
+
}
|
|
1167
|
+
this.walkFiberTree(fiber.child, parentComponentId, depth + 1);
|
|
1168
|
+
this.walkFiberTree(fiber.sibling, parentComponentId, depth);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Core accumulation logic - this is where we build up the profile.
|
|
1172
|
+
*/
|
|
1173
|
+
accumulateRender(fiber, componentId, parentComponentId, depth) {
|
|
1174
|
+
const now = Date.now();
|
|
1175
|
+
const cause = this.inferRenderCause(fiber, parentComponentId);
|
|
1176
|
+
const renderCost = this.componentsInCurrentCommit > 0 ? 1 / this.componentsInCurrentCommit : 1;
|
|
1177
|
+
let profile = this.profiles.get(componentId);
|
|
1178
|
+
if (!profile) {
|
|
1179
|
+
profile = {
|
|
1180
|
+
id: generateRenderId(),
|
|
1181
|
+
componentId,
|
|
1182
|
+
componentName: this.getComponentName(fiber),
|
|
1183
|
+
componentType: this.getComponentType(fiber),
|
|
1184
|
+
mountedAt: now,
|
|
1185
|
+
totalRenders: 0,
|
|
1186
|
+
totalRenderCost: 0,
|
|
1187
|
+
velocityWindowStart: now,
|
|
1188
|
+
velocityWindowCount: 0,
|
|
1189
|
+
causeBreakdown: createEmptyCauseBreakdown(),
|
|
1190
|
+
causeDeltaBreakdown: createEmptyCauseBreakdown(),
|
|
1191
|
+
lastEmittedRenderCount: 0,
|
|
1192
|
+
lastEmittedRenderCost: 0,
|
|
1193
|
+
lastEmitTime: now,
|
|
1194
|
+
parentCounts: /* @__PURE__ */ new Map(),
|
|
1195
|
+
depth,
|
|
1196
|
+
isSuspicious: false,
|
|
1197
|
+
// NEW
|
|
1198
|
+
propChangeStats: createEmptyPropChangeStats(),
|
|
1199
|
+
propChangeDelta: []
|
|
1200
|
+
};
|
|
1201
|
+
this.profiles.set(componentId, profile);
|
|
1202
|
+
}
|
|
1203
|
+
profile.totalRenders++;
|
|
1204
|
+
profile.totalRenderCost += renderCost;
|
|
1205
|
+
profile.causeBreakdown[cause.type]++;
|
|
1206
|
+
profile.causeDeltaBreakdown[cause.type]++;
|
|
1207
|
+
if (cause.type === "props_change" /* PROPS_CHANGE */ && cause.propChanges) {
|
|
1208
|
+
this.accumulatePropChanges(profile, cause.propChanges);
|
|
1209
|
+
}
|
|
1210
|
+
const transactionId = getCurrentTransactionId();
|
|
1211
|
+
if (transactionId) {
|
|
1212
|
+
profile.lastTransactionId = transactionId;
|
|
1213
|
+
}
|
|
1214
|
+
if (parentComponentId) {
|
|
1215
|
+
const count = (profile.parentCounts.get(parentComponentId) ?? 0) + 1;
|
|
1216
|
+
profile.parentCounts.set(parentComponentId, count);
|
|
1217
|
+
if (!profile.primaryParentId || count > (profile.parentCounts.get(profile.primaryParentId) ?? 0)) {
|
|
1218
|
+
profile.primaryParentId = parentComponentId;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
profile.depth = depth;
|
|
1222
|
+
const windowStart = now - RENDER_THRESHOLDS.VELOCITY_WINDOW_MS;
|
|
1223
|
+
if (profile.velocityWindowStart < windowStart) {
|
|
1224
|
+
profile.velocityWindowStart = now;
|
|
1225
|
+
profile.velocityWindowCount = 1;
|
|
1226
|
+
} else {
|
|
1227
|
+
profile.velocityWindowCount++;
|
|
1228
|
+
}
|
|
1229
|
+
this.updateSuspiciousFlag(profile);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* NEW: Accumulate prop change details into the profile.
|
|
1233
|
+
*/
|
|
1234
|
+
accumulatePropChanges(profile, changes) {
|
|
1235
|
+
const stats = profile.propChangeStats;
|
|
1236
|
+
for (const change of changes) {
|
|
1237
|
+
if (stats.changeCount.size >= RENDER_THRESHOLDS.MAX_PROP_KEYS_TO_TRACK && !stats.changeCount.has(change.key)) {
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
stats.changeCount.set(
|
|
1241
|
+
change.key,
|
|
1242
|
+
(stats.changeCount.get(change.key) ?? 0) + 1
|
|
1243
|
+
);
|
|
1244
|
+
if (change.referenceOnly) {
|
|
1245
|
+
stats.referenceOnlyCount.set(
|
|
1246
|
+
change.key,
|
|
1247
|
+
(stats.referenceOnlyCount.get(change.key) ?? 0) + 1
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (profile.propChangeDelta.length < RENDER_THRESHOLDS.MAX_PROP_CHANGES_PER_SNAPSHOT) {
|
|
1252
|
+
profile.propChangeDelta.push(
|
|
1253
|
+
...changes.slice(
|
|
1254
|
+
0,
|
|
1255
|
+
RENDER_THRESHOLDS.MAX_PROP_CHANGES_PER_SNAPSHOT - profile.propChangeDelta.length
|
|
1256
|
+
)
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Build prop change snapshot for emission.
|
|
1262
|
+
*/
|
|
1263
|
+
buildPropChangeSnapshot(profile) {
|
|
1264
|
+
const stats = profile.propChangeStats;
|
|
1265
|
+
if (stats.changeCount.size === 0) {
|
|
1266
|
+
return void 0;
|
|
1267
|
+
}
|
|
1268
|
+
const sorted = Array.from(stats.changeCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, RENDER_THRESHOLDS.TOP_PROPS_TO_REPORT);
|
|
1269
|
+
const topChangedProps = sorted.map(([key, count]) => {
|
|
1270
|
+
const refOnlyCount = stats.referenceOnlyCount.get(key) ?? 0;
|
|
1271
|
+
return {
|
|
1272
|
+
key,
|
|
1273
|
+
count,
|
|
1274
|
+
referenceOnlyPercent: count > 0 ? Math.round(refOnlyCount / count * 100) : 0
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1277
|
+
return { topChangedProps };
|
|
1278
|
+
}
|
|
1279
|
+
updateSuspiciousFlag(profile) {
|
|
1280
|
+
const velocity = this.calculateVelocity(profile);
|
|
1281
|
+
if (velocity > RENDER_THRESHOLDS.HOT_VELOCITY) {
|
|
1282
|
+
profile.isSuspicious = true;
|
|
1283
|
+
profile.suspiciousReason = `High render velocity: ${velocity.toFixed(
|
|
1284
|
+
1
|
|
1285
|
+
)}/sec`;
|
|
1286
|
+
} else if (profile.totalRenders > RENDER_THRESHOLDS.HIGH_RENDER_COUNT) {
|
|
1287
|
+
profile.isSuspicious = true;
|
|
1288
|
+
profile.suspiciousReason = `High total renders: ${profile.totalRenders}`;
|
|
1289
|
+
} else {
|
|
1290
|
+
profile.isSuspicious = false;
|
|
1291
|
+
profile.suspiciousReason = void 0;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Calculates renders per second from velocity window.
|
|
1296
|
+
* Cheap: just count / window duration, no array operations.
|
|
1297
|
+
*/
|
|
1298
|
+
calculateVelocity(profile) {
|
|
1299
|
+
const now = Date.now();
|
|
1300
|
+
const windowAge = now - profile.velocityWindowStart;
|
|
1301
|
+
if (windowAge > RENDER_THRESHOLDS.VELOCITY_WINDOW_MS) {
|
|
1302
|
+
return 0;
|
|
1303
|
+
}
|
|
1304
|
+
const effectiveWindowMs = Math.max(windowAge, 100);
|
|
1305
|
+
return profile.velocityWindowCount / effectiveWindowMs * 1e3;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Emits a snapshot of all profiles with deltas.
|
|
1309
|
+
*/
|
|
1310
|
+
emitSnapshot() {
|
|
1311
|
+
const now = Date.now();
|
|
1312
|
+
const snapshots = [];
|
|
1313
|
+
for (const profile of this.profiles.values()) {
|
|
1314
|
+
const rendersDelta = profile.totalRenders - profile.lastEmittedRenderCount;
|
|
1315
|
+
if (rendersDelta < RENDER_THRESHOLDS.MIN_DELTA_TO_EMIT && !profile.isSuspicious) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
const velocity = this.calculateVelocity(profile);
|
|
1319
|
+
const isMount = profile.lastEmittedRenderCount === 0;
|
|
1320
|
+
const renderCostDelta = profile.totalRenderCost - profile.lastEmittedRenderCost;
|
|
1321
|
+
const propChanges = this.buildPropChangeSnapshot(profile);
|
|
1322
|
+
snapshots.push({
|
|
1323
|
+
id: profile.id,
|
|
1324
|
+
componentId: profile.componentId,
|
|
1325
|
+
componentName: profile.componentName,
|
|
1326
|
+
componentType: profile.componentType,
|
|
1327
|
+
totalRenders: profile.totalRenders,
|
|
1328
|
+
totalRenderCost: profile.totalRenderCost,
|
|
1329
|
+
avgRenderCost: profile.totalRenderCost / profile.totalRenders,
|
|
1330
|
+
rendersDelta,
|
|
1331
|
+
renderCostDelta,
|
|
1332
|
+
renderVelocity: velocity,
|
|
1333
|
+
causeBreakdown: { ...profile.causeBreakdown },
|
|
1334
|
+
causeDeltaBreakdown: { ...profile.causeDeltaBreakdown },
|
|
1335
|
+
parentComponentId: profile.primaryParentId,
|
|
1336
|
+
depth: profile.depth,
|
|
1337
|
+
lastTransactionId: profile.lastTransactionId,
|
|
1338
|
+
isSuspicious: profile.isSuspicious,
|
|
1339
|
+
suspiciousReason: profile.suspiciousReason,
|
|
1340
|
+
renderPhase: isMount ? "mount" /* MOUNT */ : "update" /* UPDATE */,
|
|
1341
|
+
mountedAt: profile.mountedAt,
|
|
1342
|
+
propChanges
|
|
1343
|
+
});
|
|
1344
|
+
profile.lastEmittedRenderCount = profile.totalRenders;
|
|
1345
|
+
profile.lastEmittedRenderCost = profile.totalRenderCost;
|
|
1346
|
+
profile.lastEmitTime = now;
|
|
1347
|
+
profile.causeDeltaBreakdown = createEmptyCauseBreakdown();
|
|
1348
|
+
profile.propChangeDelta = [];
|
|
1349
|
+
}
|
|
1350
|
+
for (const profile of this.pendingUnmounts) {
|
|
1351
|
+
const propChanges = this.buildPropChangeSnapshot(profile);
|
|
1352
|
+
snapshots.push({
|
|
1353
|
+
id: profile.id,
|
|
1354
|
+
componentId: profile.componentId,
|
|
1355
|
+
componentName: profile.componentName,
|
|
1356
|
+
componentType: profile.componentType,
|
|
1357
|
+
totalRenders: profile.totalRenders,
|
|
1358
|
+
totalRenderCost: profile.totalRenderCost,
|
|
1359
|
+
avgRenderCost: profile.totalRenderCost / Math.max(profile.totalRenders, 1),
|
|
1360
|
+
rendersDelta: 0,
|
|
1361
|
+
renderCostDelta: 0,
|
|
1362
|
+
renderVelocity: 0,
|
|
1363
|
+
causeBreakdown: { ...profile.causeBreakdown },
|
|
1364
|
+
causeDeltaBreakdown: createEmptyCauseBreakdown(),
|
|
1365
|
+
parentComponentId: profile.primaryParentId,
|
|
1366
|
+
depth: profile.depth,
|
|
1367
|
+
lastTransactionId: profile.lastTransactionId,
|
|
1368
|
+
isSuspicious: profile.isSuspicious,
|
|
1369
|
+
suspiciousReason: profile.suspiciousReason,
|
|
1370
|
+
renderPhase: "unmount" /* UNMOUNT */,
|
|
1371
|
+
mountedAt: profile.mountedAt,
|
|
1372
|
+
unmountedAt: profile.unmountedAt,
|
|
1373
|
+
propChanges
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
this.pendingUnmounts = [];
|
|
1377
|
+
if (snapshots.length === 0) return;
|
|
1378
|
+
let message = {
|
|
1379
|
+
phase: "RENDER_SNAPSHOT",
|
|
1380
|
+
sessionId: this.getSessionId(),
|
|
1381
|
+
timestamp: now,
|
|
1382
|
+
profiles: snapshots
|
|
1383
|
+
};
|
|
1384
|
+
if (this.config?.beforeSend) {
|
|
1385
|
+
const modified = this.config.beforeSend(message);
|
|
1386
|
+
if (!modified) return;
|
|
1387
|
+
message = modified;
|
|
1388
|
+
}
|
|
1389
|
+
this.sendMessage(message);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Now returns prop change details when applicable.
|
|
1393
|
+
*/
|
|
1394
|
+
inferRenderCause(fiber, parentComponentId) {
|
|
1395
|
+
const alternate = fiber.alternate;
|
|
1396
|
+
if (!alternate) {
|
|
1397
|
+
return {
|
|
1398
|
+
type: "unknown" /* UNKNOWN */,
|
|
1399
|
+
confidence: "high" /* HIGH */
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (parentComponentId && this.currentCommitComponents.has(parentComponentId)) {
|
|
1403
|
+
const prevProps = alternate.memoizedProps;
|
|
1404
|
+
const nextProps = fiber.memoizedProps;
|
|
1405
|
+
const propsChanged = prevProps !== nextProps;
|
|
1406
|
+
if (propsChanged) {
|
|
1407
|
+
const propChanges = this.diffProps(prevProps, nextProps);
|
|
1408
|
+
return {
|
|
1409
|
+
type: "props_change" /* PROPS_CHANGE */,
|
|
1410
|
+
confidence: "medium" /* MEDIUM */,
|
|
1411
|
+
triggerId: parentComponentId,
|
|
1412
|
+
propChanges
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
type: "parent_render" /* PARENT_RENDER */,
|
|
1417
|
+
confidence: "medium" /* MEDIUM */,
|
|
1418
|
+
triggerId: parentComponentId
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (fiber.memoizedState !== alternate.memoizedState) {
|
|
1422
|
+
return {
|
|
1423
|
+
type: "state_change" /* STATE_CHANGE */,
|
|
1424
|
+
confidence: "medium" /* MEDIUM */
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
if (fiber.memoizedProps !== alternate.memoizedProps) {
|
|
1428
|
+
return {
|
|
1429
|
+
type: "context_change" /* CONTEXT_CHANGE */,
|
|
1430
|
+
confidence: "low" /* LOW */
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
return {
|
|
1434
|
+
type: "unknown" /* UNKNOWN */,
|
|
1435
|
+
confidence: "unknown" /* UNKNOWN */
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Diff props to find which keys changed and whether it's reference-only.
|
|
1440
|
+
* This is the key insight generator.
|
|
1441
|
+
*/
|
|
1442
|
+
diffProps(prevProps, nextProps) {
|
|
1443
|
+
if (!prevProps || !nextProps) {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
const changes = [];
|
|
1447
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
1448
|
+
...Object.keys(prevProps),
|
|
1449
|
+
...Object.keys(nextProps)
|
|
1450
|
+
]);
|
|
1451
|
+
const skipKeys = /* @__PURE__ */ new Set(["children", "key", "ref"]);
|
|
1452
|
+
for (const key of allKeys) {
|
|
1453
|
+
if (skipKeys.has(key)) continue;
|
|
1454
|
+
const prevValue = prevProps[key];
|
|
1455
|
+
const nextValue = nextProps[key];
|
|
1456
|
+
if (prevValue === nextValue) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const referenceOnly = this.isShallowEqual(prevValue, nextValue);
|
|
1460
|
+
changes.push({ key, referenceOnly });
|
|
1461
|
+
if (changes.length >= 10) {
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return changes;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Shallow equality check to determine if a prop is reference-only change.
|
|
1469
|
+
* We only go one level deep to keep it fast.
|
|
1470
|
+
*/
|
|
1471
|
+
isShallowEqual(a, b) {
|
|
1472
|
+
if (a === b) return true;
|
|
1473
|
+
if (typeof a !== typeof b) return false;
|
|
1474
|
+
if (a === null || b === null) return false;
|
|
1475
|
+
if (typeof a === "function" && typeof b === "function") {
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1479
|
+
if (a.length !== b.length) return false;
|
|
1480
|
+
for (let i = 0; i < a.length; i++) {
|
|
1481
|
+
if (a[i] !== b[i]) return false;
|
|
1482
|
+
}
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1486
|
+
const keysA = Object.keys(a);
|
|
1487
|
+
const keysB = Object.keys(b);
|
|
1488
|
+
if (keysA.length !== keysB.length) return false;
|
|
1489
|
+
for (const key of keysA) {
|
|
1490
|
+
if (a[key] !== b[key]) return false;
|
|
1491
|
+
}
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
return a === b;
|
|
1495
|
+
}
|
|
1496
|
+
isUserComponent(fiber) {
|
|
1497
|
+
const tag = fiber.tag;
|
|
1498
|
+
return tag === 0 /* FunctionComponent */ || tag === 1 /* ClassComponent */ || tag === 11 /* ForwardRef */ || tag === 14 /* MemoComponent */ || tag === 15 /* SimpleMemoComponent */;
|
|
1499
|
+
}
|
|
1500
|
+
didFiberRender(fiber) {
|
|
1501
|
+
return (fiber.flags & 1 /* PerformedWork */) !== 0;
|
|
1502
|
+
}
|
|
1503
|
+
getOrCreateComponentId(fiber) {
|
|
1504
|
+
let id = this.fiberToComponentId.get(fiber);
|
|
1505
|
+
if (id) return id;
|
|
1506
|
+
if (fiber.alternate) {
|
|
1507
|
+
id = this.fiberToComponentId.get(fiber.alternate);
|
|
1508
|
+
if (id) {
|
|
1509
|
+
this.fiberToComponentId.set(fiber, id);
|
|
1510
|
+
return id;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
id = `c_${++this.componentIdCounter}`;
|
|
1514
|
+
this.fiberToComponentId.set(fiber, id);
|
|
1515
|
+
return id;
|
|
1516
|
+
}
|
|
1517
|
+
getComponentName(fiber) {
|
|
1518
|
+
const type = fiber.type;
|
|
1519
|
+
if (!type) return "Unknown";
|
|
1520
|
+
if (typeof type === "function") {
|
|
1521
|
+
return type.displayName || type.name || "Anonymous";
|
|
1522
|
+
}
|
|
1523
|
+
if (typeof type === "object" && type !== null) {
|
|
1524
|
+
if (type.displayName) return type.displayName;
|
|
1525
|
+
if (type.render)
|
|
1526
|
+
return type.render.displayName || type.render.name || "ForwardRef";
|
|
1527
|
+
if (type.type) {
|
|
1528
|
+
const inner = type.type;
|
|
1529
|
+
return inner.displayName || inner.name || "Memo";
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return "Unknown";
|
|
1533
|
+
}
|
|
1534
|
+
getComponentType(fiber) {
|
|
1535
|
+
switch (fiber.tag) {
|
|
1536
|
+
case 0 /* FunctionComponent */:
|
|
1537
|
+
return "function";
|
|
1538
|
+
case 1 /* ClassComponent */:
|
|
1539
|
+
return "class";
|
|
1540
|
+
case 11 /* ForwardRef */:
|
|
1541
|
+
return "forwardRef";
|
|
1542
|
+
case 14 /* MemoComponent */:
|
|
1543
|
+
case 15 /* SimpleMemoComponent */:
|
|
1544
|
+
return "memo";
|
|
1545
|
+
default:
|
|
1546
|
+
return "unknown";
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Force emit current state (useful for debugging or on-demand refresh).
|
|
1551
|
+
*/
|
|
1552
|
+
forceEmit() {
|
|
1553
|
+
this.emitSnapshot();
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Get current profile for a component (useful for debugging).
|
|
1557
|
+
*/
|
|
1558
|
+
getProfile(componentId) {
|
|
1559
|
+
return this.profiles.get(componentId);
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Get all suspicious components.
|
|
1563
|
+
*/
|
|
1564
|
+
getSuspiciousComponents() {
|
|
1565
|
+
return Array.from(this.profiles.values()).filter((p) => p.isSuspicious);
|
|
1566
|
+
}
|
|
1567
|
+
cleanup() {
|
|
1568
|
+
if (!this.isSetup) return;
|
|
1569
|
+
this.emitSnapshot();
|
|
1570
|
+
if (this.snapshotTimer) {
|
|
1571
|
+
clearInterval(this.snapshotTimer);
|
|
1572
|
+
this.snapshotTimer = null;
|
|
1573
|
+
}
|
|
1574
|
+
if (this.originalHook) {
|
|
1575
|
+
if (this.originalOnCommitFiberRoot) {
|
|
1576
|
+
this.originalHook.onCommitFiberRoot = this.originalOnCommitFiberRoot;
|
|
1577
|
+
}
|
|
1578
|
+
if (this.originalOnCommitFiberUnmount) {
|
|
1579
|
+
this.originalHook.onCommitFiberUnmount = this.originalOnCommitFiberUnmount;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
this.originalHook = null;
|
|
1583
|
+
this.originalOnCommitFiberRoot = null;
|
|
1584
|
+
this.originalOnCommitFiberUnmount = null;
|
|
1585
|
+
this.profiles.clear();
|
|
1586
|
+
this.pendingUnmounts = [];
|
|
1587
|
+
this.currentCommitComponents.clear();
|
|
1588
|
+
this.componentIdCounter = 0;
|
|
1589
|
+
this.config = null;
|
|
1590
|
+
this.isSetup = false;
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
|
|
992
1594
|
// src/limelight/LimelightClient.ts
|
|
993
1595
|
var LimelightClient = class {
|
|
994
1596
|
ws = null;
|
|
@@ -1003,6 +1605,7 @@ var LimelightClient = class {
|
|
|
1003
1605
|
networkInterceptor;
|
|
1004
1606
|
xhrInterceptor;
|
|
1005
1607
|
consoleInterceptor;
|
|
1608
|
+
renderInterceptor;
|
|
1006
1609
|
constructor() {
|
|
1007
1610
|
this.networkInterceptor = new NetworkInterceptor(
|
|
1008
1611
|
this.sendMessage.bind(this),
|
|
@@ -1016,10 +1619,14 @@ var LimelightClient = class {
|
|
|
1016
1619
|
this.sendMessage.bind(this),
|
|
1017
1620
|
() => this.sessionId
|
|
1018
1621
|
);
|
|
1622
|
+
this.renderInterceptor = new RenderInterceptor(
|
|
1623
|
+
this.sendMessage.bind(this),
|
|
1624
|
+
() => this.sessionId
|
|
1625
|
+
);
|
|
1019
1626
|
}
|
|
1020
1627
|
/**
|
|
1021
1628
|
* Configures the Limelight client with the provided settings.
|
|
1022
|
-
* Sets up network, XHR, and
|
|
1629
|
+
* Sets up network, XHR, console, and render interceptors based on the configuration.
|
|
1023
1630
|
* @internal
|
|
1024
1631
|
* @private
|
|
1025
1632
|
* @param {LimelightConfig} config - Configuration object for Limelight
|
|
@@ -1029,11 +1636,12 @@ var LimelightClient = class {
|
|
|
1029
1636
|
const isEnabled = config.enabled ?? isDevelopment();
|
|
1030
1637
|
this.config = {
|
|
1031
1638
|
appName: "Limelight App",
|
|
1032
|
-
serverUrl:
|
|
1639
|
+
serverUrl: "wss://api.getlimelight.io/limelight",
|
|
1033
1640
|
enabled: isEnabled,
|
|
1034
1641
|
enableNetworkInspector: true,
|
|
1035
1642
|
enableConsole: true,
|
|
1036
1643
|
enableGraphQL: true,
|
|
1644
|
+
enableRenderInspector: true,
|
|
1037
1645
|
...config
|
|
1038
1646
|
};
|
|
1039
1647
|
if (!this.config.enabled) {
|
|
@@ -1048,6 +1656,9 @@ var LimelightClient = class {
|
|
|
1048
1656
|
if (this.config.enableConsole) {
|
|
1049
1657
|
this.consoleInterceptor.setup(this.config);
|
|
1050
1658
|
}
|
|
1659
|
+
if (this.config.enableRenderInspector) {
|
|
1660
|
+
this.renderInterceptor.setup(this.config);
|
|
1661
|
+
}
|
|
1051
1662
|
} catch (error) {
|
|
1052
1663
|
console.error("[Limelight] Failed to setup interceptors:", error);
|
|
1053
1664
|
}
|
|
@@ -1233,6 +1844,7 @@ var LimelightClient = class {
|
|
|
1233
1844
|
this.networkInterceptor.cleanup();
|
|
1234
1845
|
this.xhrInterceptor.cleanup();
|
|
1235
1846
|
this.consoleInterceptor.cleanup();
|
|
1847
|
+
this.renderInterceptor.cleanup();
|
|
1236
1848
|
this.reconnectAttempts = 0;
|
|
1237
1849
|
this.messageQueue = [];
|
|
1238
1850
|
}
|