@sanity/client 7.20.0 → 7.22.0

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.
@@ -64,6 +64,7 @@ export class ClientError extends Error {
64
64
  response: ErrorProps['response']
65
65
  statusCode: ErrorProps['statusCode'] = 400
66
66
  responseBody: ErrorProps['responseBody']
67
+ traceId: ErrorProps['traceId']
67
68
  details: ErrorProps['details']
68
69
 
69
70
  constructor(res: Any, context?: HttpContext) {
@@ -78,6 +79,7 @@ export class ServerError extends Error {
78
79
  response: ErrorProps['response']
79
80
  statusCode: ErrorProps['statusCode'] = 500
80
81
  responseBody: ErrorProps['responseBody']
82
+ traceId: ErrorProps['traceId']
81
83
  details: ErrorProps['details']
82
84
 
83
85
  constructor(res: Any) {
@@ -93,13 +95,14 @@ function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
93
95
  response: res,
94
96
  statusCode: res.statusCode,
95
97
  responseBody: stringifyBody(body, res),
98
+ traceId: extractTraceId(res),
96
99
  message: '',
97
100
  details: undefined as Any,
98
101
  }
99
102
 
100
103
  // Fall back early if we didn't get a JSON object returned as expected
101
104
  if (!isRecord(body)) {
102
- props.message = httpErrorMessage(res, body)
105
+ props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`
103
106
  return props
104
107
  }
105
108
 
@@ -107,18 +110,18 @@ function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
107
110
 
108
111
  // API/Boom style errors ({statusCode, error, message})
109
112
  if (typeof error === 'string' && typeof body.message === 'string') {
110
- props.message = `${error} - ${body.message}`
113
+ props.message = `${error} - ${body.message}${formatTraceId(props.traceId)}`
111
114
  return props
112
115
  }
113
116
 
114
117
  // Content Lake errors with a `error` prop being an object
115
118
  if (typeof error !== 'object' || error === null) {
116
119
  if (typeof error === 'string') {
117
- props.message = error
120
+ props.message = `${error}${formatTraceId(props.traceId)}`
118
121
  } else if (typeof body.message === 'string') {
119
- props.message = body.message
122
+ props.message = `${body.message}${formatTraceId(props.traceId)}`
120
123
  } else {
121
- props.message = httpErrorMessage(res, body)
124
+ props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`
122
125
  }
123
126
  return props
124
127
  }
@@ -134,7 +137,7 @@ function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
134
137
  if (allItems.length > MAX_ITEMS_IN_ERROR_MESSAGE) {
135
138
  itemsStr += `\n...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`
136
139
  }
137
- props.message = `${error.description}${itemsStr}`
140
+ props.message = `${error.description}${formatTraceId(props.traceId)}${itemsStr}`
138
141
  props.details = body.error
139
142
  return props
140
143
  }
@@ -142,20 +145,20 @@ function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
142
145
  // Query parse errors
143
146
  if (isQueryParseError(error)) {
144
147
  const tag = context?.options?.query?.tag
145
- props.message = formatQueryParseError(error, tag)
148
+ props.message = formatQueryParseError(error, tag, props.traceId)
146
149
  props.details = body.error
147
150
  return props
148
151
  }
149
152
 
150
153
  if ('description' in error && typeof error.description === 'string') {
151
154
  // Query/database errors ({error: {description, other, arb, props}})
152
- props.message = error.description
155
+ props.message = `${error.description}${formatTraceId(props.traceId)}`
153
156
  props.details = error
154
157
  return props
155
158
  }
156
159
 
157
160
  // Other, more arbitrary errors
158
- props.message = httpErrorMessage(res, body)
161
+ props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`
159
162
  return props
160
163
  }
161
164
 
@@ -196,17 +199,22 @@ export function isQueryParseError(error: object): error is QueryParseError {
196
199
  * @returns A formatted error message string.
197
200
  * @public
198
201
  */
199
- export function formatQueryParseError(error: QueryParseError, tag?: string | null) {
202
+ export function formatQueryParseError(
203
+ error: QueryParseError,
204
+ tag?: string | null,
205
+ traceId?: string,
206
+ ) {
200
207
  const {query, start, end, description} = error
208
+ const withTraceId = traceId ? `\n(traceId: ${traceId})` : ''
201
209
 
202
210
  if (!query || typeof start === 'undefined') {
203
- return `GROQ query parse error: ${description}`
211
+ return `GROQ query parse error: ${description}${withTraceId}`
204
212
  }
205
213
 
206
214
  const withTag = tag ? `\n\nTag: ${tag}` : ''
207
215
  const framed = codeFrame(query, {start, end}, description)
208
216
 
209
- return `GROQ query parse error:\n${framed}${withTag}`
217
+ return `GROQ query parse error:\n${framed}${withTag}${withTraceId}`
210
218
  }
211
219
 
212
220
  function httpErrorMessage(res: Any, body: unknown) {
@@ -215,12 +223,33 @@ function httpErrorMessage(res: Any, body: unknown) {
215
223
  return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`
216
224
  }
217
225
 
226
+ /**
227
+ * Extract the traceId from the traceparent header on the response.
228
+ *
229
+ * The traceparent is on the format [version]-[traceId]-[parentId]-[traceFlags], but
230
+ * when debugging end-user issues it's the traceId we need to be able to get hold of
231
+ * the relevant traces.
232
+ *
233
+ * @see https://www.w3.org/TR/trace-context/
234
+ * @returns The traceId for HTTP response
235
+ */
236
+ function extractTraceId(res: Any): string | undefined {
237
+ const traceparent = res?.headers?.['traceparent']
238
+ if (!traceparent) return
239
+
240
+ return traceparent.split('-')[1]
241
+ }
242
+
218
243
  function stringifyBody(body: Any, res: Any) {
219
244
  const contentType = (res.headers['content-type'] || '').toLowerCase()
220
245
  const isJson = contentType.indexOf('application/json') !== -1
221
246
  return isJson ? JSON.stringify(body, null, 2) : body
222
247
  }
223
248
 
249
+ function formatTraceId(traceId: string | undefined): string {
250
+ return traceId ? ` (traceId: ${traceId})` : ''
251
+ }
252
+
224
253
  function sliceWithEllipsis(str: string, max: number) {
225
254
  return str.length > max ? `${str.slice(0, max)}…` : str
226
255
  }
package/src/types.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  /* eslint-disable @typescript-eslint/no-empty-object-type */
3
3
 
4
4
  import type {Requester} from 'get-it'
5
+ import type {Observable} from 'rxjs'
5
6
 
7
+ import type {SanityClient} from './SanityClient'
6
8
  import type {InitializedStegaConfig, StegaConfig} from './stega/types'
7
9
 
8
10
  /**
@@ -203,6 +205,33 @@ export interface ClientConfig {
203
205
  * Lineage token for recursion control
204
206
  */
205
207
  lineage?: string
208
+
209
+ /**
210
+ * A custom request handler that intercepts all HTTP requests made by the client.
211
+ *
212
+ * Useful for logging, adding custom headers, refreshing auth tokens, rate limiting, etc.
213
+ *
214
+ * When using `withConfig()`, the new handler **replaces** the previous one (it does not
215
+ * wrap it). To compose handlers, you can chain them manually:
216
+ *
217
+ * ```ts
218
+ * const parent = createClient({...config, _requestHandler: handlerA})
219
+ * const child = parent.withConfig({
220
+ * _requestHandler: (req, defaultRequester) =>
221
+ * handlerB(req, (opts) => handlerA(opts, defaultRequester)),
222
+ * })
223
+ * ```
224
+ *
225
+ * Setting `_requestHandler` to `undefined` via `withConfig()` removes the handler.
226
+ *
227
+ * Note: This only applies to HTTP requests. Real-time listener connections
228
+ * (`client.listen()`) use EventSource and are not intercepted by this handler.
229
+ *
230
+ * @internal
231
+ * @deprecated Don't use outside of Sanity internals
232
+ * @see {@link RequestHandler}
233
+ */
234
+ _requestHandler?: RequestHandler
206
235
  }
207
236
 
208
237
  /** @public */
@@ -404,6 +433,7 @@ export interface ErrorProps {
404
433
  response: Any
405
434
  statusCode: number
406
435
  responseBody: Any
436
+ traceId?: string
407
437
  details: Any
408
438
  }
409
439
 
@@ -412,6 +442,38 @@ export type HttpRequest = {
412
442
  (options: RequestOptions, requester: Requester): ReturnType<Requester>
413
443
  }
414
444
 
445
+ /**
446
+ * A function that intercepts HTTP requests made by the client.
447
+ *
448
+ * Receives the resolved request options, a `defaultRequester` function that
449
+ * executes the request through the normal pipeline, and a `client` instance
450
+ * without a `_requestHandler` (to avoid recursive interception).
451
+ *
452
+ * The consumer can:
453
+ * - Modify request options before calling `defaultRequester`
454
+ * - Transform the response stream (e.g. via `pipe`)
455
+ * - Skip `defaultRequester` entirely and return a custom Observable
456
+ * - Use `client` to make additional requests (e.g. refresh an auth token on 401)
457
+ *
458
+ * When set via `withConfig()`, the new handler **replaces** (not wraps) the previous one.
459
+ *
460
+ * Note: This only applies to HTTP requests. Real-time listener connections
461
+ * (`client.listen()`) use EventSource and are not intercepted by this handler.
462
+ *
463
+ * @param request - The resolved request options including `url`
464
+ * @param defaultRequester - Executes the request through the normal pipeline
465
+ * @param client - A client instance with the same configuration but without a `_requestHandler`,
466
+ * useful for making side requests (e.g. token refresh) without triggering the handler recursively
467
+ *
468
+ * @internal
469
+ * @deprecated Don't use outside of Sanity internals
470
+ */
471
+ export type RequestHandler = (
472
+ request: RequestOptions & {url: string},
473
+ defaultRequester: (options: RequestOptions & {url: string}) => Observable<HttpRequestEvent>,
474
+ client: SanityClient,
475
+ ) => Observable<HttpRequestEvent>
476
+
415
477
  /** @internal */
416
478
  export interface RequestObservableOptions extends Omit<RequestOptions, 'url'> {
417
479
  url?: string
@@ -2059,6 +2059,7 @@
2059
2059
  response;
2060
2060
  statusCode = 400;
2061
2061
  responseBody;
2062
+ traceId;
2062
2063
  details;
2063
2064
  constructor(res, context) {
2064
2065
  const props = extractErrorProps(res, context);
@@ -2069,6 +2070,7 @@
2069
2070
  response;
2070
2071
  statusCode = 500;
2071
2072
  responseBody;
2073
+ traceId;
2072
2074
  details;
2073
2075
  constructor(res) {
2074
2076
  const props = extractErrorProps(res);
@@ -2080,29 +2082,30 @@
2080
2082
  response: res,
2081
2083
  statusCode: res.statusCode,
2082
2084
  responseBody: stringifyBody(body, res),
2085
+ traceId: extractTraceId(res),
2083
2086
  message: "",
2084
2087
  details: void 0
2085
2088
  };
2086
2089
  if (!isRecord(body))
2087
- return props.message = httpErrorMessage(res, body), props;
2090
+ return props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props;
2088
2091
  const error = body.error;
2089
2092
  if (typeof error == "string" && typeof body.message == "string")
2090
- return props.message = `${error} - ${body.message}`, props;
2093
+ return props.message = `${error} - ${body.message}${formatTraceId(props.traceId)}`, props;
2091
2094
  if (typeof error != "object" || error === null)
2092
- return typeof error == "string" ? props.message = error : typeof body.message == "string" ? props.message = body.message : props.message = httpErrorMessage(res, body), props;
2095
+ return typeof error == "string" ? props.message = `${error}${formatTraceId(props.traceId)}` : typeof body.message == "string" ? props.message = `${body.message}${formatTraceId(props.traceId)}` : props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props;
2093
2096
  if (isMutationError(error) || isActionError(error)) {
2094
2097
  const allItems = error.items || [], items = allItems.slice(0, MAX_ITEMS_IN_ERROR_MESSAGE).map((item) => item.error?.description).filter(Boolean);
2095
2098
  let itemsStr = items.length ? `:
2096
2099
  - ${items.join(`
2097
2100
  - `)}` : "";
2098
2101
  return allItems.length > MAX_ITEMS_IN_ERROR_MESSAGE && (itemsStr += `
2099
- ...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`), props.message = `${error.description}${itemsStr}`, props.details = body.error, props;
2102
+ ...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`), props.message = `${error.description}${formatTraceId(props.traceId)}${itemsStr}`, props.details = body.error, props;
2100
2103
  }
2101
2104
  if (isQueryParseError(error)) {
2102
2105
  const tag = context?.options?.query?.tag;
2103
- return props.message = formatQueryParseError(error, tag), props.details = body.error, props;
2106
+ return props.message = formatQueryParseError(error, tag, props.traceId), props.details = body.error, props;
2104
2107
  }
2105
- return "description" in error && typeof error.description == "string" ? (props.message = error.description, props.details = error, props) : (props.message = httpErrorMessage(res, body), props);
2108
+ return "description" in error && typeof error.description == "string" ? (props.message = `${error.description}${formatTraceId(props.traceId)}`, props.details = error, props) : (props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props);
2106
2109
  }
2107
2110
  function isMutationError(error) {
2108
2111
  return "type" in error && error.type === "mutationError" && "description" in error && typeof error.description == "string";
@@ -2113,23 +2116,32 @@
2113
2116
  function isQueryParseError(error) {
2114
2117
  return isRecord(error) && error.type === "queryParseError" && typeof error.query == "string" && typeof error.start == "number" && typeof error.end == "number";
2115
2118
  }
2116
- function formatQueryParseError(error, tag) {
2117
- const { query, start, end, description } = error;
2119
+ function formatQueryParseError(error, tag, traceId) {
2120
+ const { query, start, end, description } = error, withTraceId = traceId ? `
2121
+ (traceId: ${traceId})` : "";
2118
2122
  if (!query || typeof start > "u")
2119
- return `GROQ query parse error: ${description}`;
2123
+ return `GROQ query parse error: ${description}${withTraceId}`;
2120
2124
  const withTag = tag ? `
2121
2125
 
2122
2126
  Tag: ${tag}` : "";
2123
2127
  return `GROQ query parse error:
2124
- ${codeFrame(query, { start, end }, description)}${withTag}`;
2128
+ ${codeFrame(query, { start, end }, description)}${withTag}${withTraceId}`;
2125
2129
  }
2126
2130
  function httpErrorMessage(res, body) {
2127
2131
  const details = typeof body == "string" ? ` (${sliceWithEllipsis(body, 100)})` : "", statusMessage = res.statusMessage ? ` ${res.statusMessage}` : "";
2128
2132
  return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`;
2129
2133
  }
2134
+ function extractTraceId(res) {
2135
+ const traceparent = res?.headers?.traceparent;
2136
+ if (traceparent)
2137
+ return traceparent.split("-")[1];
2138
+ }
2130
2139
  function stringifyBody(body, res) {
2131
2140
  return (res.headers["content-type"] || "").toLowerCase().indexOf("application/json") !== -1 ? JSON.stringify(body, null, 2) : body;
2132
2141
  }
2142
+ function formatTraceId(traceId) {
2143
+ return traceId ? ` (traceId: ${traceId})` : "";
2144
+ }
2133
2145
  function sliceWithEllipsis(str, max) {
2134
2146
  return str.length > max ? `${str.slice(0, max)}\u2026` : str;
2135
2147
  }
@@ -3425,7 +3437,8 @@ ${selectionOpts}`);
3425
3437
  */
3426
3438
  events({
3427
3439
  includeDrafts = false,
3428
- tag: _tag
3440
+ tag: _tag,
3441
+ waitFor
3429
3442
  } = {}) {
3430
3443
  const {
3431
3444
  projectId: projectId2,
@@ -3444,7 +3457,7 @@ ${selectionOpts}`);
3444
3457
  "The live events API requires a token or withCredentials when 'includeDrafts: true'. Please update your client configuration. The token should have the lowest possible access role."
3445
3458
  );
3446
3459
  const path = _getDataUrl(this.#client, "live/events"), url = new URL(this.#client.getUrl(path, false)), tag = _tag && requestTagPrefix ? [requestTagPrefix, _tag].join(".") : _tag;
3447
- tag && url.searchParams.set("tag", tag), includeDrafts && url.searchParams.set("includeDrafts", "true");
3460
+ tag && url.searchParams.set("tag", tag), includeDrafts && url.searchParams.set("includeDrafts", "true"), waitFor && url.searchParams.set("waitFor", waitFor);
3448
3461
  const esOptions = {};
3449
3462
  includeDrafts && withCredentials && (esOptions.withCredentials = true), (includeDrafts && token || configHeaders) && (esOptions.headers = {}, includeDrafts && token && (esOptions.headers.Authorization = `Bearer ${token}`), configHeaders && Object.assign(esOptions.headers, configHeaders));
3450
3463
  const key = `${url.href}::${JSON.stringify(esOptions)}`, existing = eventsCache.get(key);
@@ -4342,13 +4355,22 @@ ${selectionOpts}`);
4342
4355
  * Private properties
4343
4356
  */
4344
4357
  #clientConfig;
4358
+ #originalHttpRequest;
4345
4359
  #httpRequest;
4346
4360
  /**
4347
4361
  * Instance properties
4348
4362
  */
4349
4363
  listen = _listen;
4350
4364
  constructor(httpRequest, config = defaultConfig) {
4351
- this.config(config), this.#httpRequest = httpRequest, this.assets = new ObservableAssetsClient(this, this.#httpRequest), this.datasets = new ObservableDatasetsClient(this, this.#httpRequest), this.live = new LiveClient(this), this.mediaLibrary = {
4365
+ this.config(config), this.#originalHttpRequest = httpRequest;
4366
+ const requestHandler = config._requestHandler;
4367
+ this.#httpRequest = requestHandler ? /* @__PURE__ */ (() => {
4368
+ let bareClient;
4369
+ return (options, requester2) => {
4370
+ const opts = options;
4371
+ return bareClient || (bareClient = new SanityClient(httpRequest, { ...config, _requestHandler: void 0 })), requestHandler(opts, (o) => httpRequest(o, requester2), bareClient);
4372
+ };
4373
+ })() : httpRequest, this.assets = new ObservableAssetsClient(this, this.#httpRequest), this.datasets = new ObservableDatasetsClient(this, this.#httpRequest), this.live = new LiveClient(this), this.mediaLibrary = {
4352
4374
  video: new ObservableMediaLibraryVideoClient(this, this.#httpRequest)
4353
4375
  }, this.projects = new ObservableProjectsClient(this, this.#httpRequest), this.users = new ObservableUsersClient(this, this.#httpRequest), this.agent = {
4354
4376
  action: new ObservableAgentsActionClient(this, this.#httpRequest)
@@ -4358,7 +4380,7 @@ ${selectionOpts}`);
4358
4380
  * Clone the client - returns a new instance
4359
4381
  */
4360
4382
  clone() {
4361
- return new ObservableSanityClient(this.#httpRequest, this.config());
4383
+ return new ObservableSanityClient(this.#originalHttpRequest, this.config());
4362
4384
  }
4363
4385
  config(newConfig) {
4364
4386
  if (newConfig === void 0)
@@ -4376,7 +4398,7 @@ ${selectionOpts}`);
4376
4398
  */
4377
4399
  withConfig(newConfig) {
4378
4400
  const thisConfig = this.config();
4379
- return new ObservableSanityClient(this.#httpRequest, {
4401
+ return new ObservableSanityClient(this.#originalHttpRequest, {
4380
4402
  ...thisConfig,
4381
4403
  ...newConfig,
4382
4404
  stega: {
@@ -4606,13 +4628,22 @@ ${selectionOpts}`);
4606
4628
  * Private properties
4607
4629
  */
4608
4630
  #clientConfig;
4631
+ #originalHttpRequest;
4609
4632
  #httpRequest;
4610
4633
  /**
4611
4634
  * Instance properties
4612
4635
  */
4613
4636
  listen = _listen;
4614
4637
  constructor(httpRequest, config = defaultConfig) {
4615
- this.config(config), this.#httpRequest = httpRequest, this.assets = new AssetsClient(this, this.#httpRequest), this.datasets = new DatasetsClient(this, this.#httpRequest), this.live = new LiveClient(this), this.mediaLibrary = {
4638
+ this.config(config), this.#originalHttpRequest = httpRequest;
4639
+ const requestHandler = config._requestHandler;
4640
+ this.#httpRequest = requestHandler ? /* @__PURE__ */ (() => {
4641
+ let bareClient;
4642
+ return (options, requester2) => {
4643
+ const opts = options;
4644
+ return bareClient || (bareClient = new SanityClient(httpRequest, { ...config, _requestHandler: void 0 })), requestHandler(opts, (o) => httpRequest(o, requester2), bareClient);
4645
+ };
4646
+ })() : httpRequest, this.assets = new AssetsClient(this, this.#httpRequest), this.datasets = new DatasetsClient(this, this.#httpRequest), this.live = new LiveClient(this), this.mediaLibrary = {
4616
4647
  video: new MediaLibraryVideoClient(this, this.#httpRequest)
4617
4648
  }, this.projects = new ProjectsClient(this, this.#httpRequest), this.users = new UsersClient(this, this.#httpRequest), this.agent = {
4618
4649
  action: new AgentActionsClient(this, this.#httpRequest)
@@ -4622,7 +4653,7 @@ ${selectionOpts}`);
4622
4653
  * Clone the client - returns a new instance
4623
4654
  */
4624
4655
  clone() {
4625
- return new SanityClient(this.#httpRequest, this.config());
4656
+ return new SanityClient(this.#originalHttpRequest, this.config());
4626
4657
  }
4627
4658
  config(newConfig) {
4628
4659
  if (newConfig === void 0)
@@ -4640,7 +4671,7 @@ ${selectionOpts}`);
4640
4671
  */
4641
4672
  withConfig(newConfig) {
4642
4673
  const thisConfig = this.config();
4643
- return new SanityClient(this.#httpRequest, {
4674
+ return new SanityClient(this.#originalHttpRequest, {
4644
4675
  ...thisConfig,
4645
4676
  ...newConfig,
4646
4677
  stega: {