@glasstrace/sdk 0.14.2 → 0.15.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.cts CHANGED
@@ -278,9 +278,15 @@ declare function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey>;
278
278
  */
279
279
  declare function loadCachedConfig(projectRoot?: string): SdkInitResponse | null;
280
280
  /**
281
- * Persists the init response to `.glasstrace/config`.
282
- * Silently skipped when `node:fs` is unavailable (non-Node environments).
283
- * On I/O failure, logs a warning and continues.
281
+ * Persists the init response to `.glasstrace/config` using atomic
282
+ * write-temp + rename semantics. Silently skipped when `node:fs` is
283
+ * unavailable (non-Node environments). On I/O failure, logs a warning.
284
+ *
285
+ * Atomicity: the payload is written to `.glasstrace/config.tmp` and then
286
+ * renamed into place. `rename` is atomic on POSIX filesystems, so readers
287
+ * either see the previous valid config or the new valid config — never a
288
+ * truncated or partially-written file (DISC-1247 Scenario 5). If the
289
+ * rename fails, the temp file is cleaned up on a best-effort basis.
284
290
  */
285
291
  declare function saveCachedConfig(response: SdkInitResponse, projectRoot?: string): Promise<void>;
286
292
  /**
package/dist/index.d.ts CHANGED
@@ -278,9 +278,15 @@ declare function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey>;
278
278
  */
279
279
  declare function loadCachedConfig(projectRoot?: string): SdkInitResponse | null;
280
280
  /**
281
- * Persists the init response to `.glasstrace/config`.
282
- * Silently skipped when `node:fs` is unavailable (non-Node environments).
283
- * On I/O failure, logs a warning and continues.
281
+ * Persists the init response to `.glasstrace/config` using atomic
282
+ * write-temp + rename semantics. Silently skipped when `node:fs` is
283
+ * unavailable (non-Node environments). On I/O failure, logs a warning.
284
+ *
285
+ * Atomicity: the payload is written to `.glasstrace/config.tmp` and then
286
+ * renamed into place. `rename` is atomic on POSIX filesystems, so readers
287
+ * either see the previous valid config or the new valid config — never a
288
+ * truncated or partially-written file (DISC-1247 Scenario 5). If the
289
+ * rename fails, the temp file is cleaned up on a best-effort basis.
284
290
  */
285
291
  declare function saveCachedConfig(response: SdkInitResponse, projectRoot?: string): Promise<void>;
286
292
  /**
package/dist/index.js CHANGED
@@ -304,6 +304,7 @@ async function saveCachedConfig(response, projectRoot) {
304
304
  const root = projectRoot ?? process.cwd();
305
305
  const dirPath = modules.path.join(root, GLASSTRACE_DIR);
306
306
  const configPath = modules.path.join(dirPath, CONFIG_FILE);
307
+ const tmpPath = `${configPath}.tmp`;
307
308
  try {
308
309
  await modules.fs.mkdir(dirPath, { recursive: true, mode: 448 });
309
310
  await modules.fs.chmod(dirPath, 448);
@@ -311,7 +312,20 @@ async function saveCachedConfig(response, projectRoot) {
311
312
  response,
312
313
  cachedAt: Date.now()
313
314
  };
314
- await modules.fs.writeFile(configPath, JSON.stringify(cached), { encoding: "utf-8", mode: 384 });
315
+ await modules.fs.writeFile(tmpPath, JSON.stringify(cached), {
316
+ encoding: "utf-8",
317
+ mode: 384
318
+ });
319
+ try {
320
+ await modules.fs.chmod(tmpPath, 384);
321
+ await modules.fs.rename(tmpPath, configPath);
322
+ } catch (renameErr) {
323
+ try {
324
+ await modules.fs.unlink(tmpPath);
325
+ } catch {
326
+ }
327
+ throw renameErr;
328
+ }
315
329
  await modules.fs.chmod(configPath, 384);
316
330
  } catch (err) {
317
331
  console.warn(
@@ -4430,7 +4444,6 @@ async function configureOtel(config, sessionManager) {
4430
4444
  await provider.shutdown();
4431
4445
  }
4432
4446
  });
4433
- registerSignalHandlers();
4434
4447
  registerBeforeExitTrigger();
4435
4448
  const prismaModule = await tryImport("@prisma/instrumentation");
4436
4449
  if (prismaModule) {
@@ -4485,12 +4498,15 @@ var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
4485
4498
  var BACKOFF_BASE_MS = HEARTBEAT_INTERVAL_MS;
4486
4499
  var BACKOFF_MAX_MS = 30 * 60 * 1e3;
4487
4500
  var BACKOFF_JITTER = 0.2;
4501
+ var HEARTBEAT_SHUTDOWN_PRIORITY = 10;
4502
+ var SHUTDOWN_MARKER_RELPATH = ".glasstrace/shutdown-requested";
4488
4503
  var heartbeatTimer = null;
4489
4504
  var heartbeatGeneration = 0;
4490
4505
  var backoffAttempts = 0;
4491
4506
  var backoffUntil = 0;
4492
4507
  var tickInProgress = false;
4493
- var _shutdownHandler = null;
4508
+ var shutdownHookRegistered = false;
4509
+ var shutdownFired = false;
4494
4510
  function startHeartbeat(config, anonKey, sdkVersion, generation, onClaimTransition) {
4495
4511
  if (heartbeatTimer !== null) return;
4496
4512
  heartbeatGeneration = generation;
@@ -4498,7 +4514,7 @@ function startHeartbeat(config, anonKey, sdkVersion, generation, onClaimTransiti
4498
4514
  void heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTransition);
4499
4515
  }, HEARTBEAT_INTERVAL_MS);
4500
4516
  heartbeatTimer.unref();
4501
- registerShutdownHandlers(config, anonKey, sdkVersion);
4517
+ registerHeartbeatShutdownHook(config, anonKey, sdkVersion);
4502
4518
  if (config.verbose) {
4503
4519
  sdkLog("info", "[glasstrace] Heartbeat started (5-minute interval).");
4504
4520
  }
@@ -4508,7 +4524,26 @@ function stopHeartbeat() {
4508
4524
  clearInterval(heartbeatTimer);
4509
4525
  heartbeatTimer = null;
4510
4526
  }
4511
- removeShutdownHandlers();
4527
+ }
4528
+ function checkShutdownMarker(projectRoot) {
4529
+ let fsSync = null;
4530
+ let pathSync = null;
4531
+ try {
4532
+ fsSync = __require("fs");
4533
+ pathSync = __require("path");
4534
+ } catch {
4535
+ return { triggered: false };
4536
+ }
4537
+ const root = projectRoot ?? (typeof process !== "undefined" ? process.cwd() : ".");
4538
+ const markerPath = pathSync.join(root, SHUTDOWN_MARKER_RELPATH);
4539
+ if (!fsSync.existsSync(markerPath)) return { triggered: false };
4540
+ try {
4541
+ fsSync.unlinkSync(markerPath);
4542
+ } catch {
4543
+ }
4544
+ const shutdown = executeShutdown().catch(() => {
4545
+ });
4546
+ return { triggered: true, shutdown };
4512
4547
  }
4513
4548
  async function heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTransition) {
4514
4549
  if (tickInProgress) return;
@@ -4518,6 +4553,14 @@ async function heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTra
4518
4553
  stopHeartbeat();
4519
4554
  return;
4520
4555
  }
4556
+ const markerResult = checkShutdownMarker();
4557
+ if (markerResult.triggered) {
4558
+ stopHeartbeat();
4559
+ if (markerResult.shutdown) {
4560
+ await markerResult.shutdown;
4561
+ }
4562
+ return;
4563
+ }
4521
4564
  if (Date.now() < backoffUntil) {
4522
4565
  if (config.verbose) {
4523
4566
  sdkLog("info", "[glasstrace] Heartbeat skipped (rate-limit backoff).");
@@ -4552,35 +4595,26 @@ async function heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTra
4552
4595
  tickInProgress = false;
4553
4596
  }
4554
4597
  }
4555
- function registerShutdownHandlers(config, anonKey, sdkVersion) {
4556
- if (typeof process === "undefined" || typeof process.once !== "function") {
4557
- return;
4558
- }
4559
- let shutdownFired = false;
4560
- const handler = (signal) => {
4561
- if (shutdownFired) return;
4562
- shutdownFired = true;
4563
- if (heartbeatTimer !== null) {
4564
- clearInterval(heartbeatTimer);
4565
- heartbeatTimer = null;
4598
+ function registerHeartbeatShutdownHook(config, anonKey, sdkVersion) {
4599
+ if (shutdownHookRegistered) return;
4600
+ shutdownHookRegistered = true;
4601
+ registerShutdownHook({
4602
+ name: "heartbeat-final-report",
4603
+ priority: HEARTBEAT_SHUTDOWN_PRIORITY,
4604
+ fn: async () => {
4605
+ if (shutdownFired) return;
4606
+ shutdownFired = true;
4607
+ if (heartbeatTimer !== null) {
4608
+ clearInterval(heartbeatTimer);
4609
+ heartbeatTimer = null;
4610
+ }
4611
+ try {
4612
+ const healthReport = collectHealthReport(sdkVersion);
4613
+ await performInit(config, anonKey, sdkVersion, healthReport);
4614
+ } catch {
4615
+ }
4566
4616
  }
4567
- const healthReport = collectHealthReport(sdkVersion);
4568
- void performInit(config, anonKey, sdkVersion, healthReport).catch(() => {
4569
- }).finally(() => {
4570
- removeShutdownHandlers();
4571
- process.kill(process.pid, signal);
4572
- });
4573
- };
4574
- _shutdownHandler = handler;
4575
- process.once("SIGTERM", _shutdownHandler);
4576
- process.once("SIGINT", _shutdownHandler);
4577
- }
4578
- function removeShutdownHandlers() {
4579
- if (_shutdownHandler && typeof process !== "undefined") {
4580
- process.removeListener("SIGTERM", _shutdownHandler);
4581
- process.removeListener("SIGINT", _shutdownHandler);
4582
- _shutdownHandler = null;
4583
- }
4617
+ });
4584
4618
  }
4585
4619
 
4586
4620
  // src/runtime-state.ts
@@ -4681,7 +4715,7 @@ function registerGlasstrace(options) {
4681
4715
  setCoreState(CoreState.REGISTERING);
4682
4716
  startRuntimeStateWriter({
4683
4717
  projectRoot: process.cwd(),
4684
- sdkVersion: "0.14.2"
4718
+ sdkVersion: "0.15.1"
4685
4719
  });
4686
4720
  const config = resolveConfig(options);
4687
4721
  if (config.verbose) {
@@ -4697,6 +4731,11 @@ function registerGlasstrace(options) {
4697
4731
  if (config.verbose) {
4698
4732
  console.info("[glasstrace] Not production-disabled.");
4699
4733
  }
4734
+ const existingProbe = trace.getTracerProvider().getTracer("glasstrace-probe");
4735
+ const anotherProviderRegistered = existingProbe.constructor.name !== "ProxyTracer";
4736
+ if (!anotherProviderRegistered) {
4737
+ registerSignalHandlers();
4738
+ }
4700
4739
  const anonymous = isAnonymousMode(config);
4701
4740
  let effectiveKey = config.apiKey;
4702
4741
  initAuthState(anonymous ? AuthState.ANONYMOUS : AuthState.AUTHENTICATED);
@@ -4727,8 +4766,6 @@ function registerGlasstrace(options) {
4727
4766
  }
4728
4767
  setCoreState(CoreState.KEY_PENDING);
4729
4768
  const currentGeneration = registrationGeneration;
4730
- const existingProbe = trace.getTracerProvider().getTracer("glasstrace-probe");
4731
- const anotherProviderRegistered = existingProbe.constructor.name !== "ProxyTracer";
4732
4769
  if (anotherProviderRegistered) {
4733
4770
  if (config.verbose) {
4734
4771
  console.info("[glasstrace] Another OTel provider detected \u2014 using existing context manager.");
@@ -4843,8 +4880,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4843
4880
  if (config.verbose) {
4844
4881
  console.info("[glasstrace] Background init firing.");
4845
4882
  }
4846
- const healthReport = collectHealthReport("0.14.2");
4847
- const initResult = await performInit(config, anonKeyForInit, "0.14.2", healthReport);
4883
+ const healthReport = collectHealthReport("0.15.1");
4884
+ const initResult = await performInit(config, anonKeyForInit, "0.15.1", healthReport);
4848
4885
  if (generation !== registrationGeneration) return;
4849
4886
  const currentState = getCoreState();
4850
4887
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4867,7 +4904,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4867
4904
  }
4868
4905
  maybeInstallConsoleCapture();
4869
4906
  if (didLastInitSucceed()) {
4870
- startHeartbeat(config, anonKeyForInit, "0.14.2", generation, (newApiKey, accountId) => {
4907
+ startHeartbeat(config, anonKeyForInit, "0.15.1", generation, (newApiKey, accountId) => {
4871
4908
  setAuthState(AuthState.CLAIMING);
4872
4909
  emitLifecycleEvent("auth:claim_started", { accountId });
4873
4910
  setResolvedApiKey(newApiKey);