Expose useful diagnostics without exposing integration secrets

Build a small authorized diagnostics response from approved fields while keeping raw exceptions, credentials, destinations, and customer data inside their owning services.

MigrationPatternAdvancedGCG engineering guide
In this guide
  1. Authorized reader
  2. Bounded diagnostics API
  3. Safe dependency check
  4. Redacted status DTO
  5. Correlation and recovery
  6. Audited access
A diagnostics boundary limits each reader to the safe status and correlation data needed for their operational decision.

Decide who needs which answer

A shopper needs a simple message when an order or integration action cannot proceed. A support engineer may need a correlation ID and a bounded state. An operations owner may need to know whether a dependency is reachable, degraded, or failing. These audiences should not receive the same endpoint or fields. A broad diagnostic response can accidentally disclose endpoints, credentials, order details, or internal topology.

Start by writing the decision each diagnostic supports. For example, an operator may need to decide whether to pause replay, while support needs to connect a customer report to an existing safe record. If a field does not help an authorized reader make a defined decision, leave it out.

  • Shopper: safe status and next action.
  • Support: correlation ID and bounded outcome.
  • Operations: dependency class, elapsed time, safe reason category.
  • Administrators: authenticated detail with audited access.

Use bounded dependency checks

Collect dependency status with a short timeout and a read-only operation approved by the provider. Limit concurrency so a health page does not flood a service that is already struggling. If you cache the result, include its checkedAt time so an operator can see how fresh it is.

serializeDiagnostics does the next step: it validates collected summaries and builds a small JSON response. It accepts only the declared dependency names, states, and reasons; canonical UTC timestamps; and elapsed times from 0 to 60,000 ms. It also rejects duplicate dependency names, more than three dependencies, and JSON larger than 2 KiB.

Call it after authorizing the operations reader in the Next.js server route. Generate an opaque UUID v4 support reference in the application. The helper neither contacts dependencies nor authorizes the route.

serialize-diagnostics.ts typescript

const names = ["erp", "search", "delivery"] as const;
const states = ["healthy", "degraded", "unavailable", "unauthorized", "unknown"] as const;
const reasons = ["ok", "timeout", "authentication", "configuration", "upstream", "pending"] as const;
type Name = typeof names[number];
type State = typeof states[number];
type Reason = typeof reasons[number];

function record(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value))
    throw new TypeError("Expected diagnostic object");
  return value as Record<string, unknown>;
}
function member<T extends string>(value: unknown, allowed: readonly T[]): T {
  if (typeof value !== "string" || !allowed.includes(value as T))
    throw new TypeError("Unsupported diagnostic category");
  return value as T;
}
function timestamp(value: unknown): string {
  if (typeof value !== "string" || value.length !== 24)
    throw new TypeError("Use UTC ISO timestamp");
  const parsed = new Date(value);
  if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value)
    throw new TypeError("Invalid UTC timestamp");
  return value;
}

// Call on the server after authorizing the operations reader.
// Input is internal data; rebuild every field before JSON serialization.
export function serializeDiagnostics(input: unknown): string {
  const source = record(input);
  if (typeof source.correlationId !== "string" ||
      !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
        .test(source.correlationId))
    throw new TypeError("Use a random UUID v4 support reference");
  if (!Array.isArray(source.dependencies) || source.dependencies.length > 3)
    throw new RangeError("Too many dependencies");
  const seen = new Set<Name>();
  const dependencies = source.dependencies.map(value => {
    const item = record(value);
    const name = member(item.name, names);
    if (seen.has(name)) throw new Error("Duplicate dependency");
    seen.add(name);
    const state: State = member(item.state, states);
    const reason: Reason = member(item.reason, reasons);
    if (typeof item.elapsedMs !== "number" || !Number.isFinite(item.elapsedMs) ||
        item.elapsedMs < 0 || item.elapsedMs > 60000)
      throw new RangeError("Invalid elapsed time");
    return { name, state, reason, checkedAt: timestamp(item.checkedAt),
      elapsedMs: Math.round(item.elapsedMs) };
  });
  const safe = { correlationId: source.correlationId, dependencies };
  const json = JSON.stringify(safe);
  if (new TextEncoder().encode(json).byteLength > 2048)
    throw new RangeError("Diagnostic response too large");
  return json;
}

For an ERP timeout, map the internal failure to name 'erp', state 'unavailable', reason 'timeout', checkedAt, and elapsedMs. An internal exception may also contain a URL or credentials, but none of those fields is copied to the output. Extra raw fields are left out; unsupported values in allowed fields cause validation to fail.

Redact by design

Build diagnostic responses from an allowlist of useful fields. Avoid serializing a request, exception, or rich service response and hoping a later filter removes its secrets. Keep authorization headers, tokens, connection strings, raw bodies, payment data, and customer identifiers out of this response.

This serializer runs in the Znode 10 Next.js/Node.js server layer. A C# health check in the Custom API can send a bounded summary through an authenticated HTTP call for this layer to format. Keep credential-bearing objects inside the service that owns them, and map any sensitive external reference to a separate support-safe identifier.

  • Allowlist response fields.
  • Cap text lengths and collection counts.
  • Authenticate and authorize each diagnostics route.
  • Record access without recording the protected content.
  • Test redaction using deliberately sensitive fixture values.

Connect status to recovery

Diagnostics are useful when they lead to an action. For a durable delivery system, show whether work is queued, retrying, dead-lettered, completed, or awaiting reconciliation. Do not promise exactly-once delivery. Provide an authenticated replay or escalation path only after confirming its authorization, idempotency, and audit requirements.

A status endpoint must distinguish an order accepted by commerce from a confirmed external completion. If the answer is unknown, say unknown and guide the operator to reconciliation. That precision prevents a support dashboard from creating false certainty.

Verification checklist

Test unauthorized access, overlong dependency responses, timeout, invalid downstream certificate or configuration, degraded state, and redaction with synthetic secret-like values. Confirm the endpoint cannot trigger a side effect and that no protected value reaches browser logs or monitoring labels. Re-review when an integration, authentication mechanism, or logging platform changes.

References and further reading

Bring your next engineering question.

Need integration observability that respects customer data?

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Data Exchanges or a legacy ERP touchpoint: choose the right starting point

Next
Next

Theme overrides without changing another store's behavior