@localstack/appinspector-ui 1.0.162 → 1.0.163

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.cjs CHANGED
@@ -2452,6 +2452,13 @@ var ConnectionError = class extends Error {
2452
2452
  this.cause = cause;
2453
2453
  }
2454
2454
  };
2455
+ var StaleTokenError = class extends Error {
2456
+ constructor(message, cause) {
2457
+ super(message);
2458
+ this.name = "StaleTokenError";
2459
+ this.cause = cause;
2460
+ }
2461
+ };
2455
2462
 
2456
2463
  // src/api/use-api.tsx
2457
2464
  var import_react = require("react");
@@ -16957,7 +16964,8 @@ var import_semver2 = __toESM(require_semver2(), 1);
16957
16964
  var ERROR_MESSAGES = {
16958
16965
  APP_INSPECTOR_DISABLED: "App Inspector is currently disabled.",
16959
16966
  APP_INSPECTOR_NOT_FOUND: "App Inspector API not found. Please ensure App Inspector is licensed and enabled.",
16960
- CONNECTION_FAILED: "Failed to connect to LocalStack. Please ensure LocalStack is running and accessible."
16967
+ CONNECTION_FAILED: "Failed to connect to LocalStack. Please ensure LocalStack is running and accessible.",
16968
+ STALE_PAGINATION_TOKEN: "Pagination token is from a previous session."
16961
16969
  };
16962
16970
 
16963
16971
  // src/utils/logger.ts
@@ -17020,6 +17028,16 @@ function unixNanoToMilliseconds(unixNanoString) {
17020
17028
  // src/api/client.ts
17021
17029
  var apiLogger = createScopedLogger("API");
17022
17030
  var analyticsLogger = createScopedLogger("ANALYTICS");
17031
+ var isStalePaginationTokenMessage = (body) => {
17032
+ let candidate = body;
17033
+ try {
17034
+ const parsed = JSON.parse(body);
17035
+ const messageField = [parsed.message, parsed.error, parsed.detail].find((value) => typeof value === "string");
17036
+ candidate = typeof messageField === "string" ? messageField : body;
17037
+ } catch {
17038
+ }
17039
+ return candidate.toLowerCase().includes("previous session");
17040
+ };
17023
17041
  var makeRequest = async (options) => {
17024
17042
  const url = getAppInspectorApiUrl(options.localstackEndpoint, options.endpoint);
17025
17043
  try {
@@ -17043,6 +17061,15 @@ var makeRequest = async (options) => {
17043
17061
  new Error(`503 from ${url}: ${response.statusText}`)
17044
17062
  );
17045
17063
  }
17064
+ if (response.status === 400) {
17065
+ const body = await response.text().catch(() => "");
17066
+ if (isStalePaginationTokenMessage(body)) {
17067
+ throw new StaleTokenError(
17068
+ ERROR_MESSAGES.STALE_PAGINATION_TOKEN,
17069
+ new Error(`400 from ${url}: ${response.statusText}`)
17070
+ );
17071
+ }
17072
+ }
17046
17073
  throw new Error(`API request failed: ${response.status.toString()} ${response.statusText}`);
17047
17074
  }
17048
17075
  options.captureHeaders?.(response.headers);
@@ -19148,6 +19175,35 @@ var AppInspectorContextProvider = (props) => {
19148
19175
  };
19149
19176
 
19150
19177
  // src/hooks/use-spans-ws.tsx
19178
+ var wsLogger = createScopedLogger("WS");
19179
+ var isLegacySpanFields = (value) => {
19180
+ return typeof value.span_id === "string" && typeof value.trace_id === "string" && typeof value.timestamp === "string";
19181
+ };
19182
+ var parseSpansWebSocketMessage = (raw) => {
19183
+ const parsed = JSON.parse(raw);
19184
+ if (typeof parsed !== "object" || parsed === null) {
19185
+ return void 0;
19186
+ }
19187
+ const record = parsed;
19188
+ if (record.type === void 0) {
19189
+ return isLegacySpanFields(record) ? { data: record, type: "span" } : void 0;
19190
+ }
19191
+ if (record.type === "epoch" || record.type === "reset") {
19192
+ return typeof record.epoch === "string" ? { epoch: record.epoch, type: record.type } : void 0;
19193
+ }
19194
+ if (record.type === "span") {
19195
+ const data = record.data;
19196
+ if (typeof data !== "object" || data === null || !isLegacySpanFields(data)) {
19197
+ return void 0;
19198
+ }
19199
+ return {
19200
+ data,
19201
+ epoch: typeof record.epoch === "string" ? record.epoch : void 0,
19202
+ type: "span"
19203
+ };
19204
+ }
19205
+ return void 0;
19206
+ };
19151
19207
  function useSpansWebSocket(options) {
19152
19208
  const { enabled, onClose, onMessage, onOpen } = options;
19153
19209
  const onMessageRef = (0, import_react46.useRef)(onMessage);
@@ -19169,6 +19225,7 @@ function useSpansWebSocket(options) {
19169
19225
  if (wsRef.current !== void 0) return;
19170
19226
  const url = new URL(getAppInspectorApiUrl(localstackEndpoint, API_ENDPOINTS.WEBSOCKET_SPANS));
19171
19227
  url.protocol = globalThis.location.protocol === "https:" ? "wss" : "ws";
19228
+ url.searchParams.set("epoch", "1");
19172
19229
  const ws = new WebSocket(url);
19173
19230
  wsRef.current = ws;
19174
19231
  ws.addEventListener("open", () => {
@@ -19176,9 +19233,20 @@ function useSpansWebSocket(options) {
19176
19233
  setConnected(true);
19177
19234
  onOpenRef.current();
19178
19235
  });
19179
- ws.addEventListener("message", () => {
19236
+ ws.addEventListener("message", (event) => {
19180
19237
  if (wsRef.current !== ws) return;
19181
- onMessageRef.current();
19238
+ let message;
19239
+ try {
19240
+ message = parseSpansWebSocketMessage(event.data);
19241
+ } catch (error) {
19242
+ wsLogger.warn("Failed to parse WS message:", { error });
19243
+ return;
19244
+ }
19245
+ if (message === void 0) {
19246
+ wsLogger.warn("Received unrecognized WS message shape:", { data: String(event.data) });
19247
+ return;
19248
+ }
19249
+ onMessageRef.current(message);
19182
19250
  });
19183
19251
  ws.addEventListener("close", () => {
19184
19252
  if (wsRef.current !== ws) return;
@@ -19222,7 +19290,7 @@ function useSpansWebSocket(options) {
19222
19290
  var PAGE_SIZE = 100;
19223
19291
  var useSpans = () => {
19224
19292
  const api = useAppInspectorApi();
19225
- const { onWsClose, onWsOpen, wsEnabled } = useAppInspectorStatus();
19293
+ const { onWsClose, onWsOpen, status, wsEnabled } = useAppInspectorStatus();
19226
19294
  const [spans, setSpans] = (0, import_react47.useState)();
19227
19295
  const [fetchError, setFetchError] = (0, import_react47.useState)();
19228
19296
  const [fetchingForward, setFetchingForward] = (0, import_react47.useState)(false);
@@ -19245,6 +19313,33 @@ var useSpans = () => {
19245
19313
  const fetchBackward = (0, import_react47.useCallback)(() => {
19246
19314
  void paginationBufferRef.current?.fetchBackward();
19247
19315
  }, []);
19316
+ const [epoch, setEpoch] = (0, import_react47.useState)();
19317
+ const onEpochSeen = (0, import_react47.useCallback)((observedEpoch) => {
19318
+ if (observedEpoch === null || observedEpoch === void 0) {
19319
+ return;
19320
+ }
19321
+ setEpoch(observedEpoch);
19322
+ }, []);
19323
+ const clearCache = (0, import_react47.useCallback)(() => {
19324
+ paginationBufferRef.current?.clear();
19325
+ setTotalCount(void 0);
19326
+ setErrorCount(void 0);
19327
+ setWarningCount(void 0);
19328
+ void paginationBufferRef.current?.fetchForward();
19329
+ }, []);
19330
+ const hasFetchedWithoutEpochRef = (0, import_react47.useRef)(false);
19331
+ const previousEpochRef = (0, import_react47.useRef)();
19332
+ (0, import_react47.useEffect)(() => {
19333
+ if (epoch === void 0) {
19334
+ return;
19335
+ }
19336
+ const isEpochChange = previousEpochRef.current !== void 0 && previousEpochRef.current !== epoch;
19337
+ const isFirstEpochAfterUnknownBackend = previousEpochRef.current === void 0 && hasFetchedWithoutEpochRef.current;
19338
+ if (isEpochChange || isFirstEpochAfterUnknownBackend) {
19339
+ clearCache();
19340
+ }
19341
+ previousEpochRef.current = epoch;
19342
+ }, [epoch, clearCache]);
19248
19343
  const applySearch = (0, import_react47.useCallback)((term) => {
19249
19344
  const normalized = term.trim();
19250
19345
  if (normalized === searchRef.current) {
@@ -19264,23 +19359,37 @@ var useSpans = () => {
19264
19359
  (0, import_react47.useEffect)(() => {
19265
19360
  const buffer = createPaginationBuffer({
19266
19361
  async fetchPage(token, signal) {
19267
- const response = await api.getSpans({
19268
- limit: PAGE_SIZE,
19269
- pagination_token: token,
19270
- search: searchRef.current || void 0
19271
- }, { signal });
19272
- setTotalCount(response.pagination.total_count);
19273
- setLicenseLimit(response.limits.span_count_limit_license);
19274
- setSystemLimit(response.limits.span_count_limit_system);
19275
- setErrorCount(response.error_count);
19276
- setWarningCount(response.warning_count);
19277
- return {
19278
- hasMoreBackward: response.pagination.has_prev,
19279
- hasMoreForward: response.pagination.has_next,
19280
- items: response.spans,
19281
- nextBackwardToken: response.pagination.prev_cursor ?? void 0,
19282
- nextForwardToken: response.pagination.next_cursor ?? void 0
19362
+ const requestPage = async (paginationToken) => {
19363
+ const response = await api.getSpans({
19364
+ limit: PAGE_SIZE,
19365
+ pagination_token: paginationToken,
19366
+ search: searchRef.current || void 0
19367
+ }, { signal });
19368
+ if (!response.pagination.epoch) {
19369
+ hasFetchedWithoutEpochRef.current = true;
19370
+ }
19371
+ onEpochSeen(response.pagination.epoch);
19372
+ setTotalCount(response.pagination.total_count);
19373
+ setLicenseLimit(response.limits.span_count_limit_license);
19374
+ setSystemLimit(response.limits.span_count_limit_system);
19375
+ setErrorCount(response.error_count);
19376
+ setWarningCount(response.warning_count);
19377
+ return {
19378
+ hasMoreBackward: response.pagination.has_prev,
19379
+ hasMoreForward: response.pagination.has_next,
19380
+ items: response.spans,
19381
+ nextBackwardToken: response.pagination.prev_cursor ?? void 0,
19382
+ nextForwardToken: response.pagination.next_cursor ?? void 0
19383
+ };
19283
19384
  };
19385
+ try {
19386
+ return await requestPage(token);
19387
+ } catch (error) {
19388
+ if (error instanceof StaleTokenError && token !== void 0) {
19389
+ return await requestPage();
19390
+ }
19391
+ throw error;
19392
+ }
19284
19393
  },
19285
19394
  onDataChange: ({ hasMoreBackward: hasMoreBackward2, hasMoreForward: hasMoreForward2, items }) => {
19286
19395
  setFetchError(void 0);
@@ -19291,9 +19400,9 @@ var useSpans = () => {
19291
19400
  onError: (error) => {
19292
19401
  setFetchError(error);
19293
19402
  },
19294
- onStatusChange(status) {
19295
- setFetchingBackward(status.fetchingBackward);
19296
- setFetchingForward(status.fetchingForward);
19403
+ onStatusChange(status2) {
19404
+ setFetchingBackward(status2.fetchingBackward);
19405
+ setFetchingForward(status2.fetchingForward);
19297
19406
  },
19298
19407
  // The spans endpoint is sorted by span ingestion time, which might differ from
19299
19408
  // the span timestamp. In order to accomodate for this, we will sort the spans
@@ -19304,7 +19413,7 @@ var useSpans = () => {
19304
19413
  });
19305
19414
  paginationBufferRef.current = buffer;
19306
19415
  void buffer.fetchForward();
19307
- }, [api]);
19416
+ }, [api, onEpochSeen]);
19308
19417
  const [clearingSpans, setClearingSpans] = (0, import_react47.useState)(false);
19309
19418
  const clearSpans = (0, import_react47.useCallback)(async () => {
19310
19419
  if (clearingSpans) {
@@ -19336,10 +19445,19 @@ var useSpans = () => {
19336
19445
  void paginationBufferRef.current?.fetchBackward();
19337
19446
  }
19338
19447
  }, [hasMoreBackward, fetchingBackward, fetchingForward]);
19448
+ const [previousStatusEpoch, setPreviousStatusEpoch] = (0, import_react47.useState)();
19449
+ if (status !== void 0 && status.epoch !== previousStatusEpoch) {
19450
+ setPreviousStatusEpoch(status.epoch);
19451
+ onEpochSeen(status.epoch);
19452
+ }
19339
19453
  useSpansWebSocket({
19340
19454
  enabled: wsEnabled,
19341
19455
  onClose: onWsClose,
19342
- onMessage: () => {
19456
+ onMessage: (message) => {
19457
+ onEpochSeen(message.epoch);
19458
+ if (message.type !== "span") {
19459
+ return;
19460
+ }
19343
19461
  if (clearingSpans) {
19344
19462
  return;
19345
19463
  }