@prezly/theme-kit-nextjs 10.9.1 → 10.9.3
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/build/adapters/prezly/cache/configure.cjs +4 -2
- package/build/adapters/prezly/cache/configure.d.ts +1 -0
- package/build/adapters/prezly/cache/configure.mjs +4 -2
- package/build/adapters/prezly/cache/redis.cjs +36 -10
- package/build/adapters/prezly/cache/redis.d.ts +3 -2
- package/build/adapters/prezly/cache/redis.mjs +36 -10
- package/build/adapters/prezly/cache/redis.test.cjs +37 -1
- package/build/adapters/prezly/cache/redis.test.mjs +37 -1
- package/build/adapters/prezly/requests.cjs +54 -8
- package/build/adapters/prezly/requests.d.ts +3 -2
- package/build/adapters/prezly/requests.mjs +55 -9
- package/build/adapters/prezly/server.cjs +14 -5
- package/build/adapters/prezly/server.d.ts +2 -1
- package/build/adapters/prezly/server.mjs +14 -5
- package/build/adapters/prezly/telemetry.cjs +56 -0
- package/build/adapters/prezly/telemetry.d.ts +4 -0
- package/build/adapters/prezly/telemetry.mjs +49 -0
- package/build/adapters/prezly/telemetry.test.cjs +167 -0
- package/build/adapters/prezly/telemetry.test.mjs +165 -0
- package/package.json +9 -9
|
@@ -25,9 +25,11 @@ function configure(config) {
|
|
|
25
25
|
return undefined;
|
|
26
26
|
}
|
|
27
27
|
function configureStorage(config) {
|
|
28
|
-
var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread({
|
|
28
|
+
var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread(_objectSpread({
|
|
29
29
|
ttl: DEFAULT_REDIS_CACHE_TTL
|
|
30
|
-
}, config.redis)
|
|
30
|
+
}, config.redis), {}, {
|
|
31
|
+
telemetry: config.telemetry
|
|
32
|
+
})) : undefined].filter(_isNotUndefined.isNotUndefined);
|
|
31
33
|
if (caches.length === 0) {
|
|
32
34
|
return undefined;
|
|
33
35
|
}
|
|
@@ -10,6 +10,7 @@ export interface Configuration {
|
|
|
10
10
|
namespace?: string;
|
|
11
11
|
/** Extra immutable identity for custom fetch implementations. Enables request sharing. */
|
|
12
12
|
requestScope?: string;
|
|
13
|
+
telemetry?: ContentDelivery.Telemetry;
|
|
13
14
|
}
|
|
14
15
|
export declare function configure(config: Configuration): {
|
|
15
16
|
storage: ContentDelivery.Cache;
|
|
@@ -19,9 +19,11 @@ export function configure(config) {
|
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
21
|
function configureStorage(config) {
|
|
22
|
-
var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread({
|
|
22
|
+
var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread(_objectSpread({
|
|
23
23
|
ttl: DEFAULT_REDIS_CACHE_TTL
|
|
24
|
-
}, config.redis)
|
|
24
|
+
}, config.redis), {}, {
|
|
25
|
+
telemetry: config.telemetry
|
|
26
|
+
})) : undefined].filter(isNotUndefined);
|
|
25
27
|
if (caches.length === 0) {
|
|
26
28
|
return undefined;
|
|
27
29
|
}
|
|
@@ -4,9 +4,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.createRedisCache = createRedisCache;
|
|
7
|
+
var _themeKitCore = require("@prezly/theme-kit-core");
|
|
7
8
|
var _jsonStableStringify = _interopRequireDefault(require("json-stable-stringify"));
|
|
8
9
|
var _redis = require("redis");
|
|
9
|
-
var _excluded = ["ttl", "prefix"];
|
|
10
|
+
var _excluded = ["ttl", "prefix", "telemetry"];
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
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; }
|
|
12
13
|
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; }
|
|
@@ -19,21 +20,32 @@ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.
|
|
|
19
20
|
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); }); }; }
|
|
20
21
|
var COMMAND_TIMEOUT = 1000;
|
|
21
22
|
var CONNECTIONS = new Map();
|
|
22
|
-
function command(_x) {
|
|
23
|
+
function command(_x, _x2) {
|
|
23
24
|
return _command.apply(this, arguments);
|
|
24
25
|
}
|
|
25
26
|
function _command() {
|
|
26
|
-
_command = _asyncToGenerator(function* (invoke) {
|
|
27
|
+
_command = _asyncToGenerator(function* (invoke, telemetry) {
|
|
28
|
+
var operation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'get';
|
|
27
29
|
var timer;
|
|
30
|
+
var start = telemetry ? _themeKitCore.ContentDelivery.telemetryNow() : 0;
|
|
31
|
+
var outcome = 'error';
|
|
28
32
|
try {
|
|
29
33
|
// node-redis 4 can corrupt its command queue when aborting a command
|
|
30
34
|
// already sent to the socket. Leave that operation tracked by the client;
|
|
31
35
|
// commandsQueueMaxLength bounds all outstanding replies, including these.
|
|
32
|
-
|
|
36
|
+
var value = yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
|
|
33
37
|
timer = setTimeout(() => reject(new Error('Content cache command timed out.')), COMMAND_TIMEOUT);
|
|
34
38
|
})]);
|
|
39
|
+
outcome = 'success';
|
|
40
|
+
return value;
|
|
35
41
|
} finally {
|
|
36
42
|
clearTimeout(timer);
|
|
43
|
+
_themeKitCore.ContentDelivery.emit(telemetry, {
|
|
44
|
+
type: 'redis_command',
|
|
45
|
+
command: operation,
|
|
46
|
+
outcome,
|
|
47
|
+
seconds: telemetry ? _themeKitCore.ContentDelivery.elapsedSeconds(start) : 0
|
|
48
|
+
});
|
|
37
49
|
}
|
|
38
50
|
});
|
|
39
51
|
return _command.apply(this, arguments);
|
|
@@ -42,6 +54,7 @@ function createRedisCache(_ref) {
|
|
|
42
54
|
var ttl = _ref.ttl,
|
|
43
55
|
_ref$prefix = _ref.prefix,
|
|
44
56
|
prefix = _ref$prefix === void 0 ? '' : _ref$prefix,
|
|
57
|
+
telemetry = _ref.telemetry,
|
|
45
58
|
options = _objectWithoutProperties(_ref, _excluded);
|
|
46
59
|
var connectionKey = (0, _jsonStableStringify.default)(options);
|
|
47
60
|
var client = CONNECTIONS.get(connectionKey);
|
|
@@ -63,29 +76,42 @@ function createRedisCache(_ref) {
|
|
|
63
76
|
function createCache() {
|
|
64
77
|
var namespacePrefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
65
78
|
return {
|
|
66
|
-
get(key, latestVersion) {
|
|
79
|
+
get(key, latestVersion, onSource) {
|
|
67
80
|
return _asyncToGenerator(function* () {
|
|
68
|
-
if (!connection.isReady)
|
|
69
|
-
|
|
81
|
+
if (!connection.isReady) {
|
|
82
|
+
_themeKitCore.ContentDelivery.emit(telemetry, {
|
|
83
|
+
type: 'redis_unavailable',
|
|
84
|
+
command: 'get'
|
|
85
|
+
});
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)), telemetry, 'get');
|
|
70
89
|
if (!cached) return undefined;
|
|
71
90
|
var entry = JSON.parse(cached);
|
|
72
91
|
if (entry.version < latestVersion) return undefined;
|
|
73
92
|
if (ttl) {
|
|
74
|
-
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
|
|
93
|
+
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
|
|
75
94
|
}
|
|
95
|
+
if (onSource) _themeKitCore.ContentDelivery.notify(onSource, 'redis');
|
|
76
96
|
return entry.value;
|
|
77
97
|
})();
|
|
78
98
|
},
|
|
79
99
|
set(key, value, version) {
|
|
80
100
|
return _asyncToGenerator(function* () {
|
|
81
|
-
if (!connection.isReady)
|
|
101
|
+
if (!connection.isReady) {
|
|
102
|
+
_themeKitCore.ContentDelivery.emit(telemetry, {
|
|
103
|
+
type: 'redis_unavailable',
|
|
104
|
+
command: 'set'
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
82
108
|
var entry = {
|
|
83
109
|
value,
|
|
84
110
|
version
|
|
85
111
|
};
|
|
86
112
|
yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
|
|
87
113
|
EX: ttl
|
|
88
|
-
}));
|
|
114
|
+
}), telemetry, 'set');
|
|
89
115
|
})();
|
|
90
116
|
},
|
|
91
117
|
namespace(namespace) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ContentDelivery } from '@prezly/theme-kit-core';
|
|
2
2
|
import { type RedisClientOptions } from 'redis';
|
|
3
3
|
type Seconds = number;
|
|
4
4
|
type Options = RedisClientOptions & {
|
|
5
5
|
ttl?: Seconds;
|
|
6
6
|
prefix?: string;
|
|
7
|
+
telemetry?: ContentDelivery.Telemetry;
|
|
7
8
|
};
|
|
8
|
-
export declare function createRedisCache({ ttl, prefix, ...options }: Options): ContentDelivery.Cache;
|
|
9
|
+
export declare function createRedisCache({ ttl, prefix, telemetry, ...options }: Options): ContentDelivery.Cache;
|
|
9
10
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _excluded = ["ttl", "prefix"];
|
|
1
|
+
var _excluded = ["ttl", "prefix", "telemetry"];
|
|
2
2
|
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; }
|
|
3
3
|
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; }
|
|
4
4
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
@@ -8,25 +8,37 @@ function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i
|
|
|
8
8
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
9
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
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
|
+
import { ContentDelivery } from '@prezly/theme-kit-core';
|
|
11
12
|
import stableStringify from 'json-stable-stringify';
|
|
12
13
|
import { createClient } from 'redis';
|
|
13
14
|
var COMMAND_TIMEOUT = 1000;
|
|
14
15
|
var CONNECTIONS = new Map();
|
|
15
|
-
function command(_x) {
|
|
16
|
+
function command(_x, _x2) {
|
|
16
17
|
return _command.apply(this, arguments);
|
|
17
18
|
}
|
|
18
19
|
function _command() {
|
|
19
|
-
_command = _asyncToGenerator(function* (invoke) {
|
|
20
|
+
_command = _asyncToGenerator(function* (invoke, telemetry) {
|
|
21
|
+
var operation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'get';
|
|
20
22
|
var timer;
|
|
23
|
+
var start = telemetry ? ContentDelivery.telemetryNow() : 0;
|
|
24
|
+
var outcome = 'error';
|
|
21
25
|
try {
|
|
22
26
|
// node-redis 4 can corrupt its command queue when aborting a command
|
|
23
27
|
// already sent to the socket. Leave that operation tracked by the client;
|
|
24
28
|
// commandsQueueMaxLength bounds all outstanding replies, including these.
|
|
25
|
-
|
|
29
|
+
var value = yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
|
|
26
30
|
timer = setTimeout(() => reject(new Error('Content cache command timed out.')), COMMAND_TIMEOUT);
|
|
27
31
|
})]);
|
|
32
|
+
outcome = 'success';
|
|
33
|
+
return value;
|
|
28
34
|
} finally {
|
|
29
35
|
clearTimeout(timer);
|
|
36
|
+
ContentDelivery.emit(telemetry, {
|
|
37
|
+
type: 'redis_command',
|
|
38
|
+
command: operation,
|
|
39
|
+
outcome,
|
|
40
|
+
seconds: telemetry ? ContentDelivery.elapsedSeconds(start) : 0
|
|
41
|
+
});
|
|
30
42
|
}
|
|
31
43
|
});
|
|
32
44
|
return _command.apply(this, arguments);
|
|
@@ -35,6 +47,7 @@ export function createRedisCache(_ref) {
|
|
|
35
47
|
var ttl = _ref.ttl,
|
|
36
48
|
_ref$prefix = _ref.prefix,
|
|
37
49
|
prefix = _ref$prefix === void 0 ? '' : _ref$prefix,
|
|
50
|
+
telemetry = _ref.telemetry,
|
|
38
51
|
options = _objectWithoutProperties(_ref, _excluded);
|
|
39
52
|
var connectionKey = stableStringify(options);
|
|
40
53
|
var client = CONNECTIONS.get(connectionKey);
|
|
@@ -56,29 +69,42 @@ export function createRedisCache(_ref) {
|
|
|
56
69
|
function createCache() {
|
|
57
70
|
var namespacePrefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
58
71
|
return {
|
|
59
|
-
get(key, latestVersion) {
|
|
72
|
+
get(key, latestVersion, onSource) {
|
|
60
73
|
return _asyncToGenerator(function* () {
|
|
61
|
-
if (!connection.isReady)
|
|
62
|
-
|
|
74
|
+
if (!connection.isReady) {
|
|
75
|
+
ContentDelivery.emit(telemetry, {
|
|
76
|
+
type: 'redis_unavailable',
|
|
77
|
+
command: 'get'
|
|
78
|
+
});
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)), telemetry, 'get');
|
|
63
82
|
if (!cached) return undefined;
|
|
64
83
|
var entry = JSON.parse(cached);
|
|
65
84
|
if (entry.version < latestVersion) return undefined;
|
|
66
85
|
if (ttl) {
|
|
67
|
-
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
|
|
86
|
+
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
|
|
68
87
|
}
|
|
88
|
+
if (onSource) ContentDelivery.notify(onSource, 'redis');
|
|
69
89
|
return entry.value;
|
|
70
90
|
})();
|
|
71
91
|
},
|
|
72
92
|
set(key, value, version) {
|
|
73
93
|
return _asyncToGenerator(function* () {
|
|
74
|
-
if (!connection.isReady)
|
|
94
|
+
if (!connection.isReady) {
|
|
95
|
+
ContentDelivery.emit(telemetry, {
|
|
96
|
+
type: 'redis_unavailable',
|
|
97
|
+
command: 'set'
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
75
101
|
var entry = {
|
|
76
102
|
value,
|
|
77
103
|
version
|
|
78
104
|
};
|
|
79
105
|
yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
|
|
80
106
|
EX: ttl
|
|
81
|
-
}));
|
|
107
|
+
}), telemetry, 'set');
|
|
82
108
|
})();
|
|
83
109
|
},
|
|
84
110
|
namespace(namespace) {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
var _redis = require("redis");
|
|
4
4
|
var _redis2 = require("./redis.cjs");
|
|
5
|
+
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; }
|
|
6
|
+
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; }
|
|
7
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
8
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
9
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
10
|
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); }
|
|
6
11
|
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); }); }; }
|
|
7
12
|
jest.mock('redis', () => ({
|
|
@@ -100,4 +105,35 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
|
|
|
100
105
|
return function (_x) {
|
|
101
106
|
return _ref6.apply(this, arguments);
|
|
102
107
|
};
|
|
103
|
-
}());
|
|
108
|
+
}());
|
|
109
|
+
it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
110
|
+
var _connection4 = connection(false),
|
|
111
|
+
client = _connection4.client,
|
|
112
|
+
options = _connection4.options;
|
|
113
|
+
var events = [];
|
|
114
|
+
var observe = event => {
|
|
115
|
+
events.push(event);
|
|
116
|
+
};
|
|
117
|
+
var cache = (0, _redis2.createRedisCache)(_objectSpread(_objectSpread({}, options), {}, {
|
|
118
|
+
telemetry: {
|
|
119
|
+
observe,
|
|
120
|
+
source: 'prezly'
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
yield cache.get('private-key', 1);
|
|
124
|
+
yield cache.set('private-key', 'private-value', 1);
|
|
125
|
+
expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
|
|
126
|
+
expect(events.some(e => e.type === 'redis_command')).toBe(false);
|
|
127
|
+
expect(client.connect).toHaveBeenCalledTimes(1);
|
|
128
|
+
client.isReady = true;
|
|
129
|
+
var source = jest.fn();
|
|
130
|
+
expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
|
|
131
|
+
expect(source).toHaveBeenCalledWith('redis');
|
|
132
|
+
yield cache.set('private-key', 'value', 2);
|
|
133
|
+
yield new Promise(resolve => setImmediate(resolve));
|
|
134
|
+
expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
|
|
135
|
+
expect(JSON.stringify(events)).not.toContain('private-');
|
|
136
|
+
expect(_redis.createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
|
|
137
|
+
telemetry: expect.anything()
|
|
138
|
+
}));
|
|
139
|
+
}));
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
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; }
|
|
2
|
+
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; }
|
|
3
|
+
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
4
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
5
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
1
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); }
|
|
2
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); }); }; }
|
|
3
8
|
import { createClient } from 'redis';
|
|
@@ -98,4 +103,35 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
|
|
|
98
103
|
return function (_x) {
|
|
99
104
|
return _ref6.apply(this, arguments);
|
|
100
105
|
};
|
|
101
|
-
}());
|
|
106
|
+
}());
|
|
107
|
+
it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
108
|
+
var _connection4 = connection(false),
|
|
109
|
+
client = _connection4.client,
|
|
110
|
+
options = _connection4.options;
|
|
111
|
+
var events = [];
|
|
112
|
+
var observe = event => {
|
|
113
|
+
events.push(event);
|
|
114
|
+
};
|
|
115
|
+
var cache = createRedisCache(_objectSpread(_objectSpread({}, options), {}, {
|
|
116
|
+
telemetry: {
|
|
117
|
+
observe,
|
|
118
|
+
source: 'prezly'
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
yield cache.get('private-key', 1);
|
|
122
|
+
yield cache.set('private-key', 'private-value', 1);
|
|
123
|
+
expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
|
|
124
|
+
expect(events.some(e => e.type === 'redis_command')).toBe(false);
|
|
125
|
+
expect(client.connect).toHaveBeenCalledTimes(1);
|
|
126
|
+
client.isReady = true;
|
|
127
|
+
var source = jest.fn();
|
|
128
|
+
expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
|
|
129
|
+
expect(source).toHaveBeenCalledWith('redis');
|
|
130
|
+
yield cache.set('private-key', 'value', 2);
|
|
131
|
+
yield new Promise(resolve => setImmediate(resolve));
|
|
132
|
+
expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
|
|
133
|
+
expect(JSON.stringify(events)).not.toContain('private-');
|
|
134
|
+
expect(createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
|
|
135
|
+
telemetry: expect.anything()
|
|
136
|
+
}));
|
|
137
|
+
}));
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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)
|
|
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)
|
|
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
|
+
}));
|
|
@@ -0,0 +1,165 @@
|
|
|
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 { createObservedFetch, upstreamRoute } from "./telemetry.mjs";
|
|
4
|
+
import { createBoundedFetch, FetchScheduler } from "./requests.mjs";
|
|
5
|
+
import { PrezlyAdapter } from "./server.mjs";
|
|
6
|
+
var nativeFetch = globalThis.fetch;
|
|
7
|
+
var tick = () => new Promise(resolve => setImmediate(resolve));
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = nativeFetch;
|
|
10
|
+
jest.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
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) => {
|
|
13
|
+
expect(upstreamRoute("https://private-origin.test".concat(path))).toBe(expected);
|
|
14
|
+
});
|
|
15
|
+
it('observes raw response headers without consuming the body or adding a signal', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
16
|
+
var events = [];
|
|
17
|
+
var response = new Response('private-body');
|
|
18
|
+
var read = jest.spyOn(response, 'arrayBuffer');
|
|
19
|
+
var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
20
|
+
return response;
|
|
21
|
+
}));
|
|
22
|
+
var options = {
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: 'private-token'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var observed = createObservedFetch(fetchMock, {
|
|
28
|
+
observe: e => {
|
|
29
|
+
events.push(e);
|
|
30
|
+
},
|
|
31
|
+
source: 'custom'
|
|
32
|
+
}, 'raw');
|
|
33
|
+
expect(yield observed('https://private-origin.test/v2/stories/private-uuid', options)).toBe(response);
|
|
34
|
+
expect(read).not.toHaveBeenCalled();
|
|
35
|
+
expect(fetchMock).toHaveBeenCalledWith('https://private-origin.test/v2/stories/private-uuid', options);
|
|
36
|
+
expect(events).toEqual([expect.objectContaining({
|
|
37
|
+
type: 'upstream',
|
|
38
|
+
client: 'raw',
|
|
39
|
+
route: 'story',
|
|
40
|
+
source: 'custom',
|
|
41
|
+
status: '2xx'
|
|
42
|
+
})]);
|
|
43
|
+
expect(JSON.stringify(events)).not.toMatch(/private-/);
|
|
44
|
+
}));
|
|
45
|
+
it('keeps telemetry outside the source/cache identity when adapters share work', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
46
|
+
var events = [];
|
|
47
|
+
var config = {
|
|
48
|
+
newsroom: 'private-room',
|
|
49
|
+
accessToken: 'private-token'
|
|
50
|
+
};
|
|
51
|
+
var cache = {
|
|
52
|
+
memory: true,
|
|
53
|
+
latestVersion: 1,
|
|
54
|
+
namespace: 'metrics-adapter-sharing'
|
|
55
|
+
};
|
|
56
|
+
globalThis.fetch = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
57
|
+
return Response.json({
|
|
58
|
+
newsroom: {
|
|
59
|
+
name: 'room'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
var observed = PrezlyAdapter.connect(config, {
|
|
64
|
+
cache,
|
|
65
|
+
telemetry: e => {
|
|
66
|
+
events.push(e);
|
|
67
|
+
}
|
|
68
|
+
}).usePrezlyClient();
|
|
69
|
+
var plain = PrezlyAdapter.connect(config, {
|
|
70
|
+
cache
|
|
71
|
+
}).usePrezlyClient();
|
|
72
|
+
yield Promise.all([observed.contentDelivery.newsroom(), plain.contentDelivery.newsroom()]);
|
|
73
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
74
|
+
yield tick();
|
|
75
|
+
yield observed.contentDelivery.newsroom();
|
|
76
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(events.filter(e => e.type === 'upstream')).toHaveLength(1);
|
|
78
|
+
expect(events.some(e => e.type === 'cache_hit')).toBe(true);
|
|
79
|
+
}));
|
|
80
|
+
it('reports queued, full, accepted and completed wait without changing admission limits', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
81
|
+
var events = [];
|
|
82
|
+
var telemetry = {
|
|
83
|
+
observe: e => {
|
|
84
|
+
events.push(e);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var scheduler = new FetchScheduler({
|
|
88
|
+
concurrency: 1,
|
|
89
|
+
queueLimit: 1
|
|
90
|
+
});
|
|
91
|
+
var release;
|
|
92
|
+
var active = scheduler.run(() => new Promise(r => {
|
|
93
|
+
release = r;
|
|
94
|
+
}), undefined, telemetry, 'story');
|
|
95
|
+
var queued = scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
96
|
+
return 42;
|
|
97
|
+
}), undefined, telemetry, 'story');
|
|
98
|
+
yield expect(scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
99
|
+
return 0;
|
|
100
|
+
}), undefined, telemetry, 'story')).rejects.toThrow('queue is full');
|
|
101
|
+
yield tick();
|
|
102
|
+
release();
|
|
103
|
+
yield active;
|
|
104
|
+
expect(yield queued).toBe(42);
|
|
105
|
+
yield tick();
|
|
106
|
+
expect(events.filter(e => e.type === 'fetch_queue').map(e => e.outcome)).toEqual(['accepted', 'queued', 'full', 'accepted']);
|
|
107
|
+
}));
|
|
108
|
+
it('reports queue timeout and body deadline independently of successful HTTP headers', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
109
|
+
jest.useFakeTimers();
|
|
110
|
+
var events = [];
|
|
111
|
+
var telemetry = {
|
|
112
|
+
observe: e => {
|
|
113
|
+
events.push(e);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var scheduler = new FetchScheduler({
|
|
117
|
+
concurrency: 1,
|
|
118
|
+
queueLimit: 1,
|
|
119
|
+
queueTimeout: 10,
|
|
120
|
+
requestTimeout: 30
|
|
121
|
+
});
|
|
122
|
+
var finish;
|
|
123
|
+
var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
124
|
+
return new Response(new ReadableStream({
|
|
125
|
+
start(controller) {
|
|
126
|
+
finish = () => controller.close();
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
}));
|
|
130
|
+
var fetch = createBoundedFetch(createObservedFetch(fetchMock, telemetry, 'content'), scheduler, telemetry);
|
|
131
|
+
var first = fetch('https://example.test/v2/stories/search');
|
|
132
|
+
var failed = expect(first).rejects.toThrow('timed out');
|
|
133
|
+
var next = fetch('https://example.test/v2/stories/search');
|
|
134
|
+
var queued = expect(next).rejects.toThrow('queue wait timed out');
|
|
135
|
+
yield jest.advanceTimersByTimeAsync(31);
|
|
136
|
+
yield failed;
|
|
137
|
+
yield queued;
|
|
138
|
+
expect(events.filter(e => e.type === 'upstream')).toEqual([expect.objectContaining({
|
|
139
|
+
status: '2xx'
|
|
140
|
+
})]);
|
|
141
|
+
expect(events.filter(e => e.type === 'fetch_timeout')).toHaveLength(1);
|
|
142
|
+
expect(events).toContainEqual(expect.objectContaining({
|
|
143
|
+
type: 'fetch_queue',
|
|
144
|
+
outcome: 'wait_timeout'
|
|
145
|
+
}));
|
|
146
|
+
finish();
|
|
147
|
+
yield jest.advanceTimersByTimeAsync(0);
|
|
148
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
149
|
+
}));
|
|
150
|
+
it('a throwing or rejecting observer cannot change successful fetch outcomes', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
151
|
+
var response = new Response('ok');
|
|
152
|
+
for (var observe of [() => {
|
|
153
|
+
throw Error('metrics');
|
|
154
|
+
}, /*#__PURE__*/_asyncToGenerator(function* () {
|
|
155
|
+
throw Error('metrics');
|
|
156
|
+
})]) {
|
|
157
|
+
var fetch = createObservedFetch(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
158
|
+
return response;
|
|
159
|
+
}), {
|
|
160
|
+
observe
|
|
161
|
+
}, 'raw');
|
|
162
|
+
expect(yield fetch('https://example.test')).toBe(response);
|
|
163
|
+
}
|
|
164
|
+
yield tick();
|
|
165
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prezly/theme-kit-nextjs",
|
|
3
|
-
"version": "10.9.
|
|
3
|
+
"version": "10.9.3",
|
|
4
4
|
"description": "Data layer and utility library for developing Prezly themes with NextJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.mjs",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
45
|
+
"node": ">= 18.18.0",
|
|
46
46
|
"npm": ">= 8.x"
|
|
47
47
|
},
|
|
48
48
|
"devEngines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">= 18.18.0",
|
|
50
50
|
"npm": ">= 8.x"
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
@@ -78,14 +78,14 @@
|
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@noble/hashes": "1.8.0",
|
|
81
|
+
"@prezly/theme-kit-core": "^10.9.3",
|
|
82
|
+
"@prezly/theme-kit-intl": "^10.9.1",
|
|
83
|
+
"@prezly/theme-kit-react": "^10.9.1",
|
|
81
84
|
"@technically/is-not-undefined": "^1.0.0",
|
|
82
85
|
"@technically/omit-undefined": "^1.0.4",
|
|
83
86
|
"json-stable-stringify": "^1.2.1",
|
|
84
87
|
"redis": "^4.7.0",
|
|
85
|
-
"url-pattern": "^1.0.3"
|
|
86
|
-
"@prezly/theme-kit-core": "^10.9.1",
|
|
87
|
-
"@prezly/theme-kit-react": "^10.9.1",
|
|
88
|
-
"@prezly/theme-kit-intl": "^10.9.1"
|
|
88
|
+
"url-pattern": "^1.0.3"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@sentry/nextjs": "7.120.4"
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"access": "public"
|
|
95
95
|
},
|
|
96
96
|
"sideEffects": false,
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "31134742a0ff3f3e669283f94843c3222eac8ebd",
|
|
98
98
|
"scripts": {
|
|
99
99
|
"prebuild": "rimraf --glob build/* *.tsbuildinfo",
|
|
100
100
|
"build": "pnpm build:types && pnpm build:esm && pnpm build:cjs",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"test:unit": "jest",
|
|
112
112
|
"test:redis": "node tests/test-redis-resilience.cjs",
|
|
113
113
|
"test:build": "pnpm test:build:cjs && pnpm test:build:esm && pnpm test:build:types",
|
|
114
|
-
"test:build:types": "tsc --esModuleInterop --noEmit build
|
|
114
|
+
"test:build:types": "tsc --esModuleInterop --noEmit --target es2020 --moduleResolution node tests/test-build-types.ts",
|
|
115
115
|
"test:build:esm": "node tests/test-build.mjs",
|
|
116
116
|
"test:build:cjs": "node tests/test-build.cjs"
|
|
117
117
|
}
|