@prezly/theme-kit-nextjs 10.9.2 → 10.10.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.
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.FetchScheduler = void 0;
7
7
  exports.createBoundedFetch = createBoundedFetch;
8
+ var _themeKitCore = require("@prezly/theme-kit-core");
9
+ var _telemetry = require("./telemetry.cjs");
8
10
  var _registry$key;
9
11
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
10
12
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
@@ -48,33 +50,62 @@ class FetchScheduler {
48
50
  }
49
51
  }
50
52
  }
51
- run(invoke, signal) {
52
- if (signal !== null && signal !== void 0 && signal.aborted) return Promise.reject(abortReason(signal));
53
+ run(invoke, signal, telemetry) {
54
+ var route = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'other';
55
+ if (signal !== null && signal !== void 0 && signal.aborted) {
56
+ _themeKitCore.ContentDelivery.emit(telemetry, {
57
+ type: 'fetch_queue',
58
+ route,
59
+ outcome: 'aborted',
60
+ seconds: 0
61
+ });
62
+ return Promise.reject(abortReason(signal));
63
+ }
53
64
  return new Promise((_resolve, reject) => {
54
65
  var task = {
55
66
  invoke,
56
67
  resolve: value => _resolve(value),
57
68
  reject,
58
69
  signal,
59
- clearQueueWait: () => {}
70
+ clearQueueWait: () => {},
71
+ telemetry,
72
+ route
60
73
  };
61
74
  if (this.active < this.limits.concurrency) {
62
75
  this.start(task);
63
76
  return;
64
77
  }
65
78
  if (this.queue.length >= this.limits.queueLimit) {
79
+ _themeKitCore.ContentDelivery.emit(telemetry, {
80
+ type: 'fetch_queue',
81
+ route,
82
+ outcome: 'full',
83
+ seconds: 0
84
+ });
66
85
  reject(new Error('Content request queue is full.'));
67
86
  return;
68
87
  }
69
- var cancel = error => {
88
+ task.queuedAt = telemetry ? _themeKitCore.ContentDelivery.telemetryNow() : undefined;
89
+ _themeKitCore.ContentDelivery.emit(telemetry, {
90
+ type: 'fetch_queue',
91
+ route,
92
+ outcome: 'queued'
93
+ });
94
+ var cancel = (error, outcome) => {
70
95
  var index = this.queue.indexOf(task);
71
96
  if (index < 0) return;
72
97
  this.queue.splice(index, 1);
73
98
  task.clearQueueWait();
99
+ _themeKitCore.ContentDelivery.emit(telemetry, {
100
+ type: 'fetch_queue',
101
+ route,
102
+ outcome,
103
+ seconds: task.queuedAt === undefined ? 0 : _themeKitCore.ContentDelivery.elapsedSeconds(task.queuedAt)
104
+ });
74
105
  reject(error);
75
106
  };
76
- var onAbort = () => cancel(abortReason(signal));
77
- var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.')), this.limits.queueTimeout);
107
+ var onAbort = () => cancel(abortReason(signal), 'aborted');
108
+ var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.'), 'wait_timeout'), this.limits.queueTimeout);
78
109
  task.clearQueueWait = () => {
79
110
  clearTimeout(timer);
80
111
  signal === null || signal === void 0 || signal.removeEventListener('abort', onAbort);
@@ -88,6 +119,12 @@ class FetchScheduler {
88
119
  start(task) {
89
120
  var _task$signal2;
90
121
  task.clearQueueWait();
122
+ _themeKitCore.ContentDelivery.emit(task.telemetry, {
123
+ type: 'fetch_queue',
124
+ route: task.route,
125
+ outcome: 'accepted',
126
+ seconds: task.queuedAt === undefined ? 0 : _themeKitCore.ContentDelivery.elapsedSeconds(task.queuedAt)
127
+ });
91
128
  this.active += 1;
92
129
  var controller = new AbortController();
93
130
  var rejectAbort = () => task.reject(abortReason(controller.signal));
@@ -101,7 +138,15 @@ class FetchScheduler {
101
138
  (_task$signal2 = task.signal) === null || _task$signal2 === void 0 || _task$signal2.addEventListener('abort', forwardAbort, {
102
139
  once: true
103
140
  });
104
- var timer = setTimeout(() => controller.abort(new Error('Content request timed out.')), this.limits.requestTimeout);
141
+ var timer = setTimeout(() => {
142
+ if (!controller.signal.aborted) {
143
+ _themeKitCore.ContentDelivery.emit(task.telemetry, {
144
+ type: 'fetch_timeout',
145
+ route: task.route
146
+ });
147
+ controller.abort(new Error('Content request timed out.'));
148
+ }
149
+ }, this.limits.requestTimeout);
105
150
  void Promise.resolve().then(() => {
106
151
  if (controller.signal.aborted) throw abortReason(controller.signal);
107
152
  return task.invoke(controller.signal);
@@ -127,6 +172,7 @@ var registry = globalThis;
127
172
  var sharedContentFetches = (_registry$key = registry[key]) !== null && _registry$key !== void 0 ? _registry$key : registry[key] = new FetchScheduler();
128
173
  function createBoundedFetch(fetchImpl) {
129
174
  var scheduler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : sharedContentFetches;
175
+ var telemetry = arguments.length > 2 ? arguments[2] : undefined;
130
176
  return (input, init) => {
131
177
  var _init$signal;
132
178
  var signal = (_init$signal = init === null || init === void 0 ? void 0 : init.signal) !== null && _init$signal !== void 0 ? _init$signal : input instanceof Request ? input.signal : undefined;
@@ -147,6 +193,6 @@ function createBoundedFetch(fetchImpl) {
147
193
  return function (_x) {
148
194
  return _ref4.apply(this, arguments);
149
195
  };
150
- }(), signal);
196
+ }(), signal, telemetry, telemetry ? (0, _telemetry.upstreamRoute)(input) : 'other');
151
197
  };
152
198
  }
@@ -10,8 +10,9 @@ export declare class FetchScheduler {
10
10
  private readonly queue;
11
11
  private readonly limits;
12
12
  constructor(limits?: Partial<Limits>);
13
- run<T>(invoke: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal | null): Promise<T>;
13
+ run<T>(invoke: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal | null, telemetry?: ContentDelivery.Telemetry, route?: ContentDelivery.UpstreamRoute): Promise<T>;
14
14
  private start;
15
15
  }
16
- export declare function createBoundedFetch(fetchImpl: typeof fetch, scheduler?: FetchScheduler): typeof fetch;
16
+ export declare function createBoundedFetch(fetchImpl: typeof fetch, scheduler?: FetchScheduler, telemetry?: ContentDelivery.Telemetry): typeof fetch;
17
+ import { ContentDelivery } from '@prezly/theme-kit-core';
17
18
  export {};
@@ -41,33 +41,62 @@ export class FetchScheduler {
41
41
  }
42
42
  }
43
43
  }
44
- run(invoke, signal) {
45
- if (signal !== null && signal !== void 0 && signal.aborted) return Promise.reject(abortReason(signal));
44
+ run(invoke, signal, telemetry) {
45
+ var route = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'other';
46
+ if (signal !== null && signal !== void 0 && signal.aborted) {
47
+ ContentDelivery.emit(telemetry, {
48
+ type: 'fetch_queue',
49
+ route,
50
+ outcome: 'aborted',
51
+ seconds: 0
52
+ });
53
+ return Promise.reject(abortReason(signal));
54
+ }
46
55
  return new Promise((_resolve, reject) => {
47
56
  var task = {
48
57
  invoke,
49
58
  resolve: value => _resolve(value),
50
59
  reject,
51
60
  signal,
52
- clearQueueWait: () => {}
61
+ clearQueueWait: () => {},
62
+ telemetry,
63
+ route
53
64
  };
54
65
  if (this.active < this.limits.concurrency) {
55
66
  this.start(task);
56
67
  return;
57
68
  }
58
69
  if (this.queue.length >= this.limits.queueLimit) {
70
+ ContentDelivery.emit(telemetry, {
71
+ type: 'fetch_queue',
72
+ route,
73
+ outcome: 'full',
74
+ seconds: 0
75
+ });
59
76
  reject(new Error('Content request queue is full.'));
60
77
  return;
61
78
  }
62
- var cancel = error => {
79
+ task.queuedAt = telemetry ? ContentDelivery.telemetryNow() : undefined;
80
+ ContentDelivery.emit(telemetry, {
81
+ type: 'fetch_queue',
82
+ route,
83
+ outcome: 'queued'
84
+ });
85
+ var cancel = (error, outcome) => {
63
86
  var index = this.queue.indexOf(task);
64
87
  if (index < 0) return;
65
88
  this.queue.splice(index, 1);
66
89
  task.clearQueueWait();
90
+ ContentDelivery.emit(telemetry, {
91
+ type: 'fetch_queue',
92
+ route,
93
+ outcome,
94
+ seconds: task.queuedAt === undefined ? 0 : ContentDelivery.elapsedSeconds(task.queuedAt)
95
+ });
67
96
  reject(error);
68
97
  };
69
- var onAbort = () => cancel(abortReason(signal));
70
- var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.')), this.limits.queueTimeout);
98
+ var onAbort = () => cancel(abortReason(signal), 'aborted');
99
+ var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.'), 'wait_timeout'), this.limits.queueTimeout);
71
100
  task.clearQueueWait = () => {
72
101
  clearTimeout(timer);
73
102
  signal === null || signal === void 0 || signal.removeEventListener('abort', onAbort);
@@ -81,6 +110,12 @@ export class FetchScheduler {
81
110
  start(task) {
82
111
  var _task$signal2;
83
112
  task.clearQueueWait();
113
+ ContentDelivery.emit(task.telemetry, {
114
+ type: 'fetch_queue',
115
+ route: task.route,
116
+ outcome: 'accepted',
117
+ seconds: task.queuedAt === undefined ? 0 : ContentDelivery.elapsedSeconds(task.queuedAt)
118
+ });
84
119
  this.active += 1;
85
120
  var controller = new AbortController();
86
121
  var rejectAbort = () => task.reject(abortReason(controller.signal));
@@ -94,7 +129,15 @@ export class FetchScheduler {
94
129
  (_task$signal2 = task.signal) === null || _task$signal2 === void 0 || _task$signal2.addEventListener('abort', forwardAbort, {
95
130
  once: true
96
131
  });
97
- var timer = setTimeout(() => controller.abort(new Error('Content request timed out.')), this.limits.requestTimeout);
132
+ var timer = setTimeout(() => {
133
+ if (!controller.signal.aborted) {
134
+ ContentDelivery.emit(task.telemetry, {
135
+ type: 'fetch_timeout',
136
+ route: task.route
137
+ });
138
+ controller.abort(new Error('Content request timed out.'));
139
+ }
140
+ }, this.limits.requestTimeout);
98
141
  void Promise.resolve().then(() => {
99
142
  if (controller.signal.aborted) throw abortReason(controller.signal);
100
143
  return task.invoke(controller.signal);
@@ -119,6 +162,7 @@ var registry = globalThis;
119
162
  var sharedContentFetches = (_registry$key = registry[key]) !== null && _registry$key !== void 0 ? _registry$key : registry[key] = new FetchScheduler();
120
163
  export function createBoundedFetch(fetchImpl) {
121
164
  var scheduler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : sharedContentFetches;
165
+ var telemetry = arguments.length > 2 ? arguments[2] : undefined;
122
166
  return (input, init) => {
123
167
  var _init$signal;
124
168
  var signal = (_init$signal = init === null || init === void 0 ? void 0 : init.signal) !== null && _init$signal !== void 0 ? _init$signal : input instanceof Request ? input.signal : undefined;
@@ -139,6 +183,8 @@ export function createBoundedFetch(fetchImpl) {
139
183
  return function (_x) {
140
184
  return _ref4.apply(this, arguments);
141
185
  };
142
- }(), signal);
186
+ }(), signal, telemetry, telemetry ? upstreamRoute(input) : 'other');
143
187
  };
144
- }
188
+ }
189
+ import { ContentDelivery } from '@prezly/theme-kit-core';
190
+ import { upstreamRoute } from "./telemetry.mjs";
@@ -11,6 +11,7 @@ var _sdk = require("@prezly/sdk");
11
11
  var _themeKitCore = require("@prezly/theme-kit-core");
12
12
  var _index = require("./cache/index.cjs");
13
13
  var _requests = require("./requests.cjs");
14
+ var _telemetry = require("./telemetry.cjs");
14
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
16
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
16
17
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -22,7 +23,8 @@ var PrezlyAdapter;
22
23
  function connect(config) {
23
24
  var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
24
25
  cacheConfig = _ref.cache,
25
- fetch = _ref.fetch;
26
+ fetch = _ref.fetch,
27
+ observer = _ref.telemetry;
26
28
  function usePrezlyClient() {
27
29
  var _cacheConfig$namespac, _cacheConfig$memory;
28
30
  var _Resolvable$resolve = _themeKitCore.Resolvable.resolve(config),
@@ -32,15 +34,21 @@ var PrezlyAdapter;
32
34
  newsroom = _Resolvable$resolve.newsroom,
33
35
  theme = _Resolvable$resolve.theme,
34
36
  formats = _Resolvable$resolve.formats;
37
+ var telemetry = observer ? {
38
+ observe: observer,
39
+ source: !baseUrl || baseUrl.replace(/\/$/, '') === 'https://api.prezly.com' ? 'prezly' : 'custom'
40
+ } : undefined;
35
41
  var client = (0, _sdk.createPrezlyClient)({
36
- fetch,
42
+ fetch: telemetry ? (0, _telemetry.createObservedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'raw') : fetch,
37
43
  accessToken,
38
44
  baseUrl,
39
45
  headers
40
46
  });
41
- var cache = cacheConfig ? (0, _index.configure)(_objectSpread({
47
+ var cache = cacheConfig ? (0, _index.configure)(_objectSpread(_objectSpread({
42
48
  namespace: 'content:'
43
- }, cacheConfig)) : undefined;
49
+ }, cacheConfig), {}, {
50
+ telemetry
51
+ })) : undefined;
44
52
  // Custom fetches may depend on hidden request context. Preserve their
45
53
  // existing cache behavior unless the caller declares that identity.
46
54
  var scope = cache && (!fetch || (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.requestScope) !== undefined) ? (0, _utils.bytesToHex)((0, _sha.sha256)((0, _jsonStableStringify.default)({
@@ -58,10 +66,11 @@ var PrezlyAdapter;
58
66
  accessToken,
59
67
  baseUrl,
60
68
  headers,
61
- fetch: (0, _requests.createBoundedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch)
69
+ fetch: (0, _requests.createBoundedFetch)((0, _telemetry.createObservedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'content'), undefined, telemetry)
62
70
  }) : client;
63
71
  var contentDelivery = _themeKitCore.ContentDelivery.createClient(contentClient, newsroom, theme, {
64
72
  formats,
73
+ telemetry,
65
74
  cache: cache ? _objectSpread(_objectSpread({}, cache), {}, {
66
75
  scope
67
76
  }) : undefined
@@ -14,9 +14,10 @@ export declare namespace PrezlyAdapter {
14
14
  interface Options {
15
15
  cache?: CacheConfiguration;
16
16
  fetch?: typeof fetch;
17
+ telemetry?: ContentDelivery.TelemetryObserver;
17
18
  }
18
19
  type CacheConfiguration = CacheConfig;
19
- function connect(config: Resolvable<Configuration>, { cache: cacheConfig, fetch }?: Options): {
20
+ function connect(config: Resolvable<Configuration>, { cache: cacheConfig, fetch, telemetry: observer }?: Options): {
20
21
  usePrezlyClient: () => {
21
22
  client: import("@prezly/sdk").PrezlyClient;
22
23
  contentDelivery: {
@@ -10,12 +10,14 @@ import { createPrezlyClient } from '@prezly/sdk';
10
10
  import { ContentDelivery, Resolvable } from '@prezly/theme-kit-core';
11
11
  import { configure as configureCache } from "./cache/index.mjs";
12
12
  import { createBoundedFetch } from "./requests.mjs";
13
+ import { createObservedFetch } from "./telemetry.mjs";
13
14
  export var PrezlyAdapter;
14
15
  (function (_PrezlyAdapter) {
15
16
  function connect(config) {
16
17
  var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
17
18
  cacheConfig = _ref.cache,
18
- fetch = _ref.fetch;
19
+ fetch = _ref.fetch,
20
+ observer = _ref.telemetry;
19
21
  function usePrezlyClient() {
20
22
  var _cacheConfig$namespac, _cacheConfig$memory;
21
23
  var _Resolvable$resolve = Resolvable.resolve(config),
@@ -25,15 +27,21 @@ export var PrezlyAdapter;
25
27
  newsroom = _Resolvable$resolve.newsroom,
26
28
  theme = _Resolvable$resolve.theme,
27
29
  formats = _Resolvable$resolve.formats;
30
+ var telemetry = observer ? {
31
+ observe: observer,
32
+ source: !baseUrl || baseUrl.replace(/\/$/, '') === 'https://api.prezly.com' ? 'prezly' : 'custom'
33
+ } : undefined;
28
34
  var client = createPrezlyClient({
29
- fetch,
35
+ fetch: telemetry ? createObservedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'raw') : fetch,
30
36
  accessToken,
31
37
  baseUrl,
32
38
  headers
33
39
  });
34
- var cache = cacheConfig ? configureCache(_objectSpread({
40
+ var cache = cacheConfig ? configureCache(_objectSpread(_objectSpread({
35
41
  namespace: 'content:'
36
- }, cacheConfig)) : undefined;
42
+ }, cacheConfig), {}, {
43
+ telemetry
44
+ })) : undefined;
37
45
  // Custom fetches may depend on hidden request context. Preserve their
38
46
  // existing cache behavior unless the caller declares that identity.
39
47
  var scope = cache && (!fetch || (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.requestScope) !== undefined) ? bytesToHex(sha256(stableStringify({
@@ -51,10 +59,11 @@ export var PrezlyAdapter;
51
59
  accessToken,
52
60
  baseUrl,
53
61
  headers,
54
- fetch: createBoundedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch)
62
+ fetch: createBoundedFetch(createObservedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'content'), undefined, telemetry)
55
63
  }) : client;
56
64
  var contentDelivery = ContentDelivery.createClient(contentClient, newsroom, theme, {
57
65
  formats,
66
+ telemetry,
58
67
  cache: cache ? _objectSpread(_objectSpread({}, cache), {}, {
59
68
  scope
60
69
  }) : undefined
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createObservedFetch = createObservedFetch;
7
+ exports.upstreamRoute = upstreamRoute;
8
+ var _themeKitCore = require("@prezly/theme-kit-core");
9
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
10
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
11
+ function upstreamRoute(input) {
12
+ try {
13
+ var path = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url).pathname;
14
+ if (/^\/v2\/stories\/by-slug\/[^/]+$/.test(path)) return 'story_by_slug';
15
+ if (/^\/v2\/stories(?:\/search)?$/.test(path)) return 'stories';
16
+ if (/^\/v2\/stories\/[^/]+$/.test(path)) return 'story';
17
+ if (/^\/v2\/newsrooms\/[^/]+\/themes\/[^/]+$/.test(path)) return 'theme';
18
+ if (/^\/v2\/newsrooms\/[^/]+\/languages(?:\/[^/]+)?$/.test(path)) return 'languages';
19
+ if (/^\/v2\/newsrooms\/[^/]+\/categories(?:\/[^/]+)?$/.test(path)) return 'categories';
20
+ if (/^\/v2\/newsrooms\/[^/]+\/contacts(?:\/search)?$/.test(path)) return 'contacts';
21
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries(?:\/search)?$/.test(path)) return 'galleries';
22
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries\/[^/]+$/.test(path)) return 'gallery';
23
+ if (/^\/v2\/newsrooms\/[^/]+$/.test(path)) return 'newsroom';
24
+ } catch (_unused) {
25
+ // Invalid/custom inputs keep their original fetch behavior.
26
+ }
27
+ return 'other';
28
+ }
29
+
30
+ /** Observe response headers only; preserve the Response and streaming body. */
31
+ function createObservedFetch(fetchImpl, telemetry, client) {
32
+ if (!telemetry) return fetchImpl;
33
+ return /*#__PURE__*/function () {
34
+ var _ref = _asyncToGenerator(function* (input, init) {
35
+ var route = upstreamRoute(input);
36
+ var start = _themeKitCore.ContentDelivery.telemetryNow();
37
+ var status = 'error';
38
+ try {
39
+ var response = yield fetchImpl(input, init);
40
+ status = response.status >= 200 && response.status < 600 ? "".concat(Math.floor(response.status / 100), "xx") : 'other';
41
+ return response;
42
+ } finally {
43
+ _themeKitCore.ContentDelivery.emit(telemetry, {
44
+ type: 'upstream',
45
+ route,
46
+ client,
47
+ status,
48
+ seconds: _themeKitCore.ContentDelivery.elapsedSeconds(start)
49
+ });
50
+ }
51
+ });
52
+ return function (_x, _x2) {
53
+ return _ref.apply(this, arguments);
54
+ };
55
+ }();
56
+ }
@@ -0,0 +1,4 @@
1
+ import { ContentDelivery } from '@prezly/theme-kit-core';
2
+ export declare function upstreamRoute(input: Parameters<typeof fetch>[0]): ContentDelivery.UpstreamRoute;
3
+ /** Observe response headers only; preserve the Response and streaming body. */
4
+ export declare function createObservedFetch(fetchImpl: typeof fetch, telemetry: ContentDelivery.Telemetry | undefined, client: 'raw' | 'content'): typeof fetch;
@@ -0,0 +1,49 @@
1
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
2
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
3
+ import { ContentDelivery } from '@prezly/theme-kit-core';
4
+ export function upstreamRoute(input) {
5
+ try {
6
+ var path = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url).pathname;
7
+ if (/^\/v2\/stories\/by-slug\/[^/]+$/.test(path)) return 'story_by_slug';
8
+ if (/^\/v2\/stories(?:\/search)?$/.test(path)) return 'stories';
9
+ if (/^\/v2\/stories\/[^/]+$/.test(path)) return 'story';
10
+ if (/^\/v2\/newsrooms\/[^/]+\/themes\/[^/]+$/.test(path)) return 'theme';
11
+ if (/^\/v2\/newsrooms\/[^/]+\/languages(?:\/[^/]+)?$/.test(path)) return 'languages';
12
+ if (/^\/v2\/newsrooms\/[^/]+\/categories(?:\/[^/]+)?$/.test(path)) return 'categories';
13
+ if (/^\/v2\/newsrooms\/[^/]+\/contacts(?:\/search)?$/.test(path)) return 'contacts';
14
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries(?:\/search)?$/.test(path)) return 'galleries';
15
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries\/[^/]+$/.test(path)) return 'gallery';
16
+ if (/^\/v2\/newsrooms\/[^/]+$/.test(path)) return 'newsroom';
17
+ } catch (_unused) {
18
+ // Invalid/custom inputs keep their original fetch behavior.
19
+ }
20
+ return 'other';
21
+ }
22
+
23
+ /** Observe response headers only; preserve the Response and streaming body. */
24
+ export function createObservedFetch(fetchImpl, telemetry, client) {
25
+ if (!telemetry) return fetchImpl;
26
+ return /*#__PURE__*/function () {
27
+ var _ref = _asyncToGenerator(function* (input, init) {
28
+ var route = upstreamRoute(input);
29
+ var start = ContentDelivery.telemetryNow();
30
+ var status = 'error';
31
+ try {
32
+ var response = yield fetchImpl(input, init);
33
+ status = response.status >= 200 && response.status < 600 ? "".concat(Math.floor(response.status / 100), "xx") : 'other';
34
+ return response;
35
+ } finally {
36
+ ContentDelivery.emit(telemetry, {
37
+ type: 'upstream',
38
+ route,
39
+ client,
40
+ status,
41
+ seconds: ContentDelivery.elapsedSeconds(start)
42
+ });
43
+ }
44
+ });
45
+ return function (_x, _x2) {
46
+ return _ref.apply(this, arguments);
47
+ };
48
+ }();
49
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ var _telemetry = require("./telemetry.cjs");
4
+ var _requests = require("./requests.cjs");
5
+ var _server = require("./server.cjs");
6
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
7
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
8
+ var nativeFetch = globalThis.fetch;
9
+ var tick = () => new Promise(resolve => setImmediate(resolve));
10
+ afterEach(() => {
11
+ globalThis.fetch = nativeFetch;
12
+ jest.useRealTimers();
13
+ });
14
+ it.each([['/v2/newsrooms/private-id', 'newsroom'], ['/v2/newsrooms/private-id/languages/en_GB', 'languages'], ['/v2/newsrooms/private-id/categories', 'categories'], ['/v2/stories/by-slug/private-slug?token=secret', 'story_by_slug'], ['/v2/stories/search', 'stories'], ['/v2/stories/private-uuid', 'story'], ['/custom/private-path', 'other']])('classifies %s without retaining URL values', (path, expected) => {
15
+ expect((0, _telemetry.upstreamRoute)("https://private-origin.test".concat(path))).toBe(expected);
16
+ });
17
+ it('observes raw response headers without consuming the body or adding a signal', /*#__PURE__*/_asyncToGenerator(function* () {
18
+ var events = [];
19
+ var response = new Response('private-body');
20
+ var read = jest.spyOn(response, 'arrayBuffer');
21
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
22
+ return response;
23
+ }));
24
+ var options = {
25
+ headers: {
26
+ Authorization: 'private-token'
27
+ }
28
+ };
29
+ var observed = (0, _telemetry.createObservedFetch)(fetchMock, {
30
+ observe: e => {
31
+ events.push(e);
32
+ },
33
+ source: 'custom'
34
+ }, 'raw');
35
+ expect(yield observed('https://private-origin.test/v2/stories/private-uuid', options)).toBe(response);
36
+ expect(read).not.toHaveBeenCalled();
37
+ expect(fetchMock).toHaveBeenCalledWith('https://private-origin.test/v2/stories/private-uuid', options);
38
+ expect(events).toEqual([expect.objectContaining({
39
+ type: 'upstream',
40
+ client: 'raw',
41
+ route: 'story',
42
+ source: 'custom',
43
+ status: '2xx'
44
+ })]);
45
+ expect(JSON.stringify(events)).not.toMatch(/private-/);
46
+ }));
47
+ it('keeps telemetry outside the source/cache identity when adapters share work', /*#__PURE__*/_asyncToGenerator(function* () {
48
+ var events = [];
49
+ var config = {
50
+ newsroom: 'private-room',
51
+ accessToken: 'private-token'
52
+ };
53
+ var cache = {
54
+ memory: true,
55
+ latestVersion: 1,
56
+ namespace: 'metrics-adapter-sharing'
57
+ };
58
+ globalThis.fetch = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
59
+ return Response.json({
60
+ newsroom: {
61
+ name: 'room'
62
+ }
63
+ });
64
+ }));
65
+ var observed = _server.PrezlyAdapter.connect(config, {
66
+ cache,
67
+ telemetry: e => {
68
+ events.push(e);
69
+ }
70
+ }).usePrezlyClient();
71
+ var plain = _server.PrezlyAdapter.connect(config, {
72
+ cache
73
+ }).usePrezlyClient();
74
+ yield Promise.all([observed.contentDelivery.newsroom(), plain.contentDelivery.newsroom()]);
75
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
76
+ yield tick();
77
+ yield observed.contentDelivery.newsroom();
78
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
79
+ expect(events.filter(e => e.type === 'upstream')).toHaveLength(1);
80
+ expect(events.some(e => e.type === 'cache_hit')).toBe(true);
81
+ }));
82
+ it('reports queued, full, accepted and completed wait without changing admission limits', /*#__PURE__*/_asyncToGenerator(function* () {
83
+ var events = [];
84
+ var telemetry = {
85
+ observe: e => {
86
+ events.push(e);
87
+ }
88
+ };
89
+ var scheduler = new _requests.FetchScheduler({
90
+ concurrency: 1,
91
+ queueLimit: 1
92
+ });
93
+ var release;
94
+ var active = scheduler.run(() => new Promise(r => {
95
+ release = r;
96
+ }), undefined, telemetry, 'story');
97
+ var queued = scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
98
+ return 42;
99
+ }), undefined, telemetry, 'story');
100
+ yield expect(scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
101
+ return 0;
102
+ }), undefined, telemetry, 'story')).rejects.toThrow('queue is full');
103
+ yield tick();
104
+ release();
105
+ yield active;
106
+ expect(yield queued).toBe(42);
107
+ yield tick();
108
+ expect(events.filter(e => e.type === 'fetch_queue').map(e => e.outcome)).toEqual(['accepted', 'queued', 'full', 'accepted']);
109
+ }));
110
+ it('reports queue timeout and body deadline independently of successful HTTP headers', /*#__PURE__*/_asyncToGenerator(function* () {
111
+ jest.useFakeTimers();
112
+ var events = [];
113
+ var telemetry = {
114
+ observe: e => {
115
+ events.push(e);
116
+ }
117
+ };
118
+ var scheduler = new _requests.FetchScheduler({
119
+ concurrency: 1,
120
+ queueLimit: 1,
121
+ queueTimeout: 10,
122
+ requestTimeout: 30
123
+ });
124
+ var finish;
125
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
126
+ return new Response(new ReadableStream({
127
+ start(controller) {
128
+ finish = () => controller.close();
129
+ }
130
+ }));
131
+ }));
132
+ var fetch = (0, _requests.createBoundedFetch)((0, _telemetry.createObservedFetch)(fetchMock, telemetry, 'content'), scheduler, telemetry);
133
+ var first = fetch('https://example.test/v2/stories/search');
134
+ var failed = expect(first).rejects.toThrow('timed out');
135
+ var next = fetch('https://example.test/v2/stories/search');
136
+ var queued = expect(next).rejects.toThrow('queue wait timed out');
137
+ yield jest.advanceTimersByTimeAsync(31);
138
+ yield failed;
139
+ yield queued;
140
+ expect(events.filter(e => e.type === 'upstream')).toEqual([expect.objectContaining({
141
+ status: '2xx'
142
+ })]);
143
+ expect(events.filter(e => e.type === 'fetch_timeout')).toHaveLength(1);
144
+ expect(events).toContainEqual(expect.objectContaining({
145
+ type: 'fetch_queue',
146
+ outcome: 'wait_timeout'
147
+ }));
148
+ finish();
149
+ yield jest.advanceTimersByTimeAsync(0);
150
+ expect(jest.getTimerCount()).toBe(0);
151
+ }));
152
+ it('a throwing or rejecting observer cannot change successful fetch outcomes', /*#__PURE__*/_asyncToGenerator(function* () {
153
+ var response = new Response('ok');
154
+ for (var observe of [() => {
155
+ throw Error('metrics');
156
+ }, /*#__PURE__*/_asyncToGenerator(function* () {
157
+ throw Error('metrics');
158
+ })]) {
159
+ var fetch = (0, _telemetry.createObservedFetch)(/*#__PURE__*/_asyncToGenerator(function* () {
160
+ return response;
161
+ }), {
162
+ observe
163
+ }, 'raw');
164
+ expect(yield fetch('https://example.test')).toBe(response);
165
+ }
166
+ yield tick();
167
+ }));