@localstack/appinspector-ui 1.0.161 → 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.js CHANGED
@@ -2448,6 +2448,13 @@ var ConnectionError = class extends Error {
2448
2448
  this.cause = cause;
2449
2449
  }
2450
2450
  };
2451
+ var StaleTokenError = class extends Error {
2452
+ constructor(message, cause) {
2453
+ super(message);
2454
+ this.name = "StaleTokenError";
2455
+ this.cause = cause;
2456
+ }
2457
+ };
2451
2458
 
2452
2459
  // src/api/use-api.tsx
2453
2460
  import { createContext, useContext } from "react";
@@ -16981,7 +16988,8 @@ var import_semver2 = __toESM(require_semver2(), 1);
16981
16988
  var ERROR_MESSAGES = {
16982
16989
  APP_INSPECTOR_DISABLED: "App Inspector is currently disabled.",
16983
16990
  APP_INSPECTOR_NOT_FOUND: "App Inspector API not found. Please ensure App Inspector is licensed and enabled.",
16984
- CONNECTION_FAILED: "Failed to connect to LocalStack. Please ensure LocalStack is running and accessible."
16991
+ CONNECTION_FAILED: "Failed to connect to LocalStack. Please ensure LocalStack is running and accessible.",
16992
+ STALE_PAGINATION_TOKEN: "Pagination token is from a previous session."
16985
16993
  };
16986
16994
 
16987
16995
  // src/utils/logger.ts
@@ -17044,6 +17052,16 @@ function unixNanoToMilliseconds(unixNanoString) {
17044
17052
  // src/api/client.ts
17045
17053
  var apiLogger = createScopedLogger("API");
17046
17054
  var analyticsLogger = createScopedLogger("ANALYTICS");
17055
+ var isStalePaginationTokenMessage = (body) => {
17056
+ let candidate = body;
17057
+ try {
17058
+ const parsed = JSON.parse(body);
17059
+ const messageField = [parsed.message, parsed.error, parsed.detail].find((value) => typeof value === "string");
17060
+ candidate = typeof messageField === "string" ? messageField : body;
17061
+ } catch {
17062
+ }
17063
+ return candidate.toLowerCase().includes("previous session");
17064
+ };
17047
17065
  var makeRequest = async (options) => {
17048
17066
  const url = getAppInspectorApiUrl(options.localstackEndpoint, options.endpoint);
17049
17067
  try {
@@ -17067,6 +17085,15 @@ var makeRequest = async (options) => {
17067
17085
  new Error(`503 from ${url}: ${response.statusText}`)
17068
17086
  );
17069
17087
  }
17088
+ if (response.status === 400) {
17089
+ const body = await response.text().catch(() => "");
17090
+ if (isStalePaginationTokenMessage(body)) {
17091
+ throw new StaleTokenError(
17092
+ ERROR_MESSAGES.STALE_PAGINATION_TOKEN,
17093
+ new Error(`400 from ${url}: ${response.statusText}`)
17094
+ );
17095
+ }
17096
+ }
17070
17097
  throw new Error(`API request failed: ${response.status.toString()} ${response.statusText}`);
17071
17098
  }
17072
17099
  options.captureHeaders?.(response.headers);
@@ -19172,6 +19199,35 @@ var AppInspectorContextProvider = (props) => {
19172
19199
  };
19173
19200
 
19174
19201
  // src/hooks/use-spans-ws.tsx
19202
+ var wsLogger = createScopedLogger("WS");
19203
+ var isLegacySpanFields = (value) => {
19204
+ return typeof value.span_id === "string" && typeof value.trace_id === "string" && typeof value.timestamp === "string";
19205
+ };
19206
+ var parseSpansWebSocketMessage = (raw) => {
19207
+ const parsed = JSON.parse(raw);
19208
+ if (typeof parsed !== "object" || parsed === null) {
19209
+ return void 0;
19210
+ }
19211
+ const record = parsed;
19212
+ if (record.type === void 0) {
19213
+ return isLegacySpanFields(record) ? { data: record, type: "span" } : void 0;
19214
+ }
19215
+ if (record.type === "epoch" || record.type === "reset") {
19216
+ return typeof record.epoch === "string" ? { epoch: record.epoch, type: record.type } : void 0;
19217
+ }
19218
+ if (record.type === "span") {
19219
+ const data = record.data;
19220
+ if (typeof data !== "object" || data === null || !isLegacySpanFields(data)) {
19221
+ return void 0;
19222
+ }
19223
+ return {
19224
+ data,
19225
+ epoch: typeof record.epoch === "string" ? record.epoch : void 0,
19226
+ type: "span"
19227
+ };
19228
+ }
19229
+ return void 0;
19230
+ };
19175
19231
  function useSpansWebSocket(options) {
19176
19232
  const { enabled, onClose, onMessage, onOpen } = options;
19177
19233
  const onMessageRef = useRef9(onMessage);
@@ -19193,6 +19249,7 @@ function useSpansWebSocket(options) {
19193
19249
  if (wsRef.current !== void 0) return;
19194
19250
  const url = new URL(getAppInspectorApiUrl(localstackEndpoint, API_ENDPOINTS.WEBSOCKET_SPANS));
19195
19251
  url.protocol = globalThis.location.protocol === "https:" ? "wss" : "ws";
19252
+ url.searchParams.set("epoch", "1");
19196
19253
  const ws = new WebSocket(url);
19197
19254
  wsRef.current = ws;
19198
19255
  ws.addEventListener("open", () => {
@@ -19200,9 +19257,20 @@ function useSpansWebSocket(options) {
19200
19257
  setConnected(true);
19201
19258
  onOpenRef.current();
19202
19259
  });
19203
- ws.addEventListener("message", () => {
19260
+ ws.addEventListener("message", (event) => {
19204
19261
  if (wsRef.current !== ws) return;
19205
- onMessageRef.current();
19262
+ let message;
19263
+ try {
19264
+ message = parseSpansWebSocketMessage(event.data);
19265
+ } catch (error) {
19266
+ wsLogger.warn("Failed to parse WS message:", { error });
19267
+ return;
19268
+ }
19269
+ if (message === void 0) {
19270
+ wsLogger.warn("Received unrecognized WS message shape:", { data: String(event.data) });
19271
+ return;
19272
+ }
19273
+ onMessageRef.current(message);
19206
19274
  });
19207
19275
  ws.addEventListener("close", () => {
19208
19276
  if (wsRef.current !== ws) return;
@@ -19246,7 +19314,7 @@ function useSpansWebSocket(options) {
19246
19314
  var PAGE_SIZE = 100;
19247
19315
  var useSpans = () => {
19248
19316
  const api = useAppInspectorApi();
19249
- const { onWsClose, onWsOpen, wsEnabled } = useAppInspectorStatus();
19317
+ const { onWsClose, onWsOpen, status, wsEnabled } = useAppInspectorStatus();
19250
19318
  const [spans, setSpans] = useState11();
19251
19319
  const [fetchError, setFetchError] = useState11();
19252
19320
  const [fetchingForward, setFetchingForward] = useState11(false);
@@ -19269,6 +19337,33 @@ var useSpans = () => {
19269
19337
  const fetchBackward = useCallback7(() => {
19270
19338
  void paginationBufferRef.current?.fetchBackward();
19271
19339
  }, []);
19340
+ const [epoch, setEpoch] = useState11();
19341
+ const onEpochSeen = useCallback7((observedEpoch) => {
19342
+ if (observedEpoch === null || observedEpoch === void 0) {
19343
+ return;
19344
+ }
19345
+ setEpoch(observedEpoch);
19346
+ }, []);
19347
+ const clearCache = useCallback7(() => {
19348
+ paginationBufferRef.current?.clear();
19349
+ setTotalCount(void 0);
19350
+ setErrorCount(void 0);
19351
+ setWarningCount(void 0);
19352
+ void paginationBufferRef.current?.fetchForward();
19353
+ }, []);
19354
+ const hasFetchedWithoutEpochRef = useRef10(false);
19355
+ const previousEpochRef = useRef10();
19356
+ useEffect16(() => {
19357
+ if (epoch === void 0) {
19358
+ return;
19359
+ }
19360
+ const isEpochChange = previousEpochRef.current !== void 0 && previousEpochRef.current !== epoch;
19361
+ const isFirstEpochAfterUnknownBackend = previousEpochRef.current === void 0 && hasFetchedWithoutEpochRef.current;
19362
+ if (isEpochChange || isFirstEpochAfterUnknownBackend) {
19363
+ clearCache();
19364
+ }
19365
+ previousEpochRef.current = epoch;
19366
+ }, [epoch, clearCache]);
19272
19367
  const applySearch = useCallback7((term) => {
19273
19368
  const normalized = term.trim();
19274
19369
  if (normalized === searchRef.current) {
@@ -19288,23 +19383,37 @@ var useSpans = () => {
19288
19383
  useEffect16(() => {
19289
19384
  const buffer = createPaginationBuffer({
19290
19385
  async fetchPage(token, signal) {
19291
- const response = await api.getSpans({
19292
- limit: PAGE_SIZE,
19293
- pagination_token: token,
19294
- search: searchRef.current || void 0
19295
- }, { signal });
19296
- setTotalCount(response.pagination.total_count);
19297
- setLicenseLimit(response.limits.span_count_limit_license);
19298
- setSystemLimit(response.limits.span_count_limit_system);
19299
- setErrorCount(response.error_count);
19300
- setWarningCount(response.warning_count);
19301
- return {
19302
- hasMoreBackward: response.pagination.has_prev,
19303
- hasMoreForward: response.pagination.has_next,
19304
- items: response.spans,
19305
- nextBackwardToken: response.pagination.prev_cursor ?? void 0,
19306
- nextForwardToken: response.pagination.next_cursor ?? void 0
19386
+ const requestPage = async (paginationToken) => {
19387
+ const response = await api.getSpans({
19388
+ limit: PAGE_SIZE,
19389
+ pagination_token: paginationToken,
19390
+ search: searchRef.current || void 0
19391
+ }, { signal });
19392
+ if (!response.pagination.epoch) {
19393
+ hasFetchedWithoutEpochRef.current = true;
19394
+ }
19395
+ onEpochSeen(response.pagination.epoch);
19396
+ setTotalCount(response.pagination.total_count);
19397
+ setLicenseLimit(response.limits.span_count_limit_license);
19398
+ setSystemLimit(response.limits.span_count_limit_system);
19399
+ setErrorCount(response.error_count);
19400
+ setWarningCount(response.warning_count);
19401
+ return {
19402
+ hasMoreBackward: response.pagination.has_prev,
19403
+ hasMoreForward: response.pagination.has_next,
19404
+ items: response.spans,
19405
+ nextBackwardToken: response.pagination.prev_cursor ?? void 0,
19406
+ nextForwardToken: response.pagination.next_cursor ?? void 0
19407
+ };
19307
19408
  };
19409
+ try {
19410
+ return await requestPage(token);
19411
+ } catch (error) {
19412
+ if (error instanceof StaleTokenError && token !== void 0) {
19413
+ return await requestPage();
19414
+ }
19415
+ throw error;
19416
+ }
19308
19417
  },
19309
19418
  onDataChange: ({ hasMoreBackward: hasMoreBackward2, hasMoreForward: hasMoreForward2, items }) => {
19310
19419
  setFetchError(void 0);
@@ -19315,9 +19424,9 @@ var useSpans = () => {
19315
19424
  onError: (error) => {
19316
19425
  setFetchError(error);
19317
19426
  },
19318
- onStatusChange(status) {
19319
- setFetchingBackward(status.fetchingBackward);
19320
- setFetchingForward(status.fetchingForward);
19427
+ onStatusChange(status2) {
19428
+ setFetchingBackward(status2.fetchingBackward);
19429
+ setFetchingForward(status2.fetchingForward);
19321
19430
  },
19322
19431
  // The spans endpoint is sorted by span ingestion time, which might differ from
19323
19432
  // the span timestamp. In order to accomodate for this, we will sort the spans
@@ -19328,7 +19437,7 @@ var useSpans = () => {
19328
19437
  });
19329
19438
  paginationBufferRef.current = buffer;
19330
19439
  void buffer.fetchForward();
19331
- }, [api]);
19440
+ }, [api, onEpochSeen]);
19332
19441
  const [clearingSpans, setClearingSpans] = useState11(false);
19333
19442
  const clearSpans = useCallback7(async () => {
19334
19443
  if (clearingSpans) {
@@ -19360,10 +19469,19 @@ var useSpans = () => {
19360
19469
  void paginationBufferRef.current?.fetchBackward();
19361
19470
  }
19362
19471
  }, [hasMoreBackward, fetchingBackward, fetchingForward]);
19472
+ const [previousStatusEpoch, setPreviousStatusEpoch] = useState11();
19473
+ if (status !== void 0 && status.epoch !== previousStatusEpoch) {
19474
+ setPreviousStatusEpoch(status.epoch);
19475
+ onEpochSeen(status.epoch);
19476
+ }
19363
19477
  useSpansWebSocket({
19364
19478
  enabled: wsEnabled,
19365
19479
  onClose: onWsClose,
19366
- onMessage: () => {
19480
+ onMessage: (message) => {
19481
+ onEpochSeen(message.epoch);
19482
+ if (message.type !== "span") {
19483
+ return;
19484
+ }
19367
19485
  if (clearingSpans) {
19368
19486
  return;
19369
19487
  }