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