Skip to content

Commit 286775e

Browse files
Fix recordPerformance on Cloudflare Workers; add per-call timeout
- await responses().put() on Workers so it flushes inside ctx.waitUntil (the fire-and-forget put was cancelled when the isolate terminated) - send sampleRate in the put body to bypass server-side gating - expose optional timeout for recordPerformance (default 1500ms) - thread timeout through Resource.put → httpPUT → httpRequest - extract isCloudflareWorkers detection to common/runtime.ts - README: document timeout, fix stale factor example, add Workers note Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 40fb7bd commit 286775e

23 files changed

Lines changed: 402 additions & 305 deletions

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,10 @@ await gatekeeper.recordPerformance();
296296
297297
// With custom options
298298
await gatekeeper.recordPerformance({
299-
sample: 0.2, // Sample 20% of requests
300-
factor: 100 // Custom timing factor
299+
sample: 1, // Record 100% of requests (default 0.2)
300+
statusCode: 200, // HTTP status code (default 200)
301+
overrideElapsed: 1234, // Custom timing in ms
302+
timeout: 1500 // Per-call API timeout in ms (default 1500)
301303
});
302304
```
303305
@@ -666,7 +668,7 @@ export default {
666668
**Workers vs. Express/Lambda — what's different:**
667669
668670
- Workers have no mutable response object. Build the outgoing `Response` yourself using values from `result` (`cookieValue`, `targetURL`, `setCookie`) rather than relying on helper methods that mutate a response in place.
669-
- Use `ctx.waitUntil()` for `recordPerformance()` so the metric call doesn't delay the user's response.
671+
- Use `ctx.waitUntil()` for `recordPerformance()` so the metric call doesn't delay the user's response. On Workers the SDK awaits the underlying API call internally (so it actually flushes inside `ctx.waitUntil`); the put is capped at 1500ms by default — pass `{ timeout: <ms> }` to tune.
670672
- Default `mode: 'full'` (used above) only needs the public key. Hybrid mode is supported but requires shipping your private key as a Worker secret — only do this if you've assessed the trade-off.
671673
672674
### React / Next.js
@@ -739,7 +741,8 @@ await gatekeeper.recordPerformance();
739741
await gatekeeper.recordPerformance({
740742
sample: 1.0, // Record 100% of requests (default 0.2)
741743
statusCode: 200, // HTTP status code
742-
overrideElapsed: 1234 // Custom timing in ms
744+
overrideElapsed: 1234, // Custom timing in ms
745+
timeout: 1500 // Per-call API timeout in ms (default 1500, overrides global SDK timeout)
743746
});
744747
```
745748

dist/client/base_client.js

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,11 @@ var axios_1 = __importDefault(require("axios"));
5555
var zod_1 = require("zod");
5656
var logger_1 = require("../common/logger");
5757
var errors_1 = require("../common/errors");
58-
/**
59-
* Detect if we're running in the Cloudflare Workers (workerd) runtime.
60-
* Workers sets navigator.userAgent to "Cloudflare-Workers" — this is the
61-
* documented and stable detection signal:
62-
* https://developers.cloudflare.com/workers/runtime-apis/web-standards/
63-
*
64-
* axios 0.27.2 has no fetch adapter and requires Node's http module, so it
65-
* crashes inside Workers. When we detect Workers, route HTTP through native
66-
* fetch instead — preserved error shape so errorHandler keeps working.
67-
*/
68-
var isCloudflareWorkers = typeof navigator !== "undefined" &&
69-
navigator.userAgent === "Cloudflare-Workers";
58+
var runtime_1 = require("../common/runtime");
59+
// axios 0.27.2 has no fetch adapter and requires Node's http module, so it
60+
// crashes inside Workers. When isCloudflareWorkers is true we route HTTP
61+
// through native fetch instead — preserved error shape so errorHandler keeps
62+
// working.
7063
var APIResponse = zod_1.z.object({}).catchall(zod_1.z.any());
7164
var APIErrorResponse = zod_1.z
7265
.object({
@@ -82,7 +75,7 @@ var BaseClient = /** @class */ (function () {
8275
this.apiUrl = options.apiUrl || apiUrl;
8376
this.key = key;
8477
this.timeout = options.timeout || 5000;
85-
if (!isCloudflareWorkers) {
78+
if (!runtime_1.isCloudflareWorkers) {
8679
// axios.defaults is process-global state and is meaningless in Workers
8780
// (we don't use axios there). Skip in Workers to avoid touching axios's
8881
// internal config which can drag in Node-only deps during import.
@@ -96,29 +89,32 @@ var BaseClient = /** @class */ (function () {
9689
* downstream work unchanged.
9790
*/
9891
BaseClient.prototype.httpRequest = function (method, url, options) {
92+
var _a;
9993
if (options === void 0) { options = {}; }
10094
return __awaiter(this, void 0, void 0, function () {
101-
var response_1, finalUrl, search, _i, _a, _b, k, v, init, hasContentType, controller, timeoutId, response, err_1, wrapped, contentType, data, _c, text, headersObj_1, wrapped, headersObj;
102-
return __generator(this, function (_d) {
103-
switch (_d.label) {
95+
var requestTimeout, response_1, finalUrl, search, _i, _b, _c, k, v, init, hasContentType, controller, timeoutId, response, err_1, wrapped, contentType, data, _d, text, headersObj_1, wrapped, headersObj;
96+
return __generator(this, function (_e) {
97+
switch (_e.label) {
10498
case 0:
105-
if (!!isCloudflareWorkers) return [3 /*break*/, 2];
99+
requestTimeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : this.timeout;
100+
if (!!runtime_1.isCloudflareWorkers) return [3 /*break*/, 2];
106101
return [4 /*yield*/, axios_1.default.request({
107102
method: method,
108103
url: url,
109104
params: options.params,
110105
data: options.body,
111106
headers: options.headers,
107+
timeout: requestTimeout,
112108
})];
113109
case 1:
114-
response_1 = _d.sent();
110+
response_1 = _e.sent();
115111
return [2 /*return*/, { data: response_1.data, status: response_1.status, headers: response_1.headers }];
116112
case 2:
117113
finalUrl = url;
118114
if (options.params && Object.keys(options.params).length > 0) {
119115
search = new URLSearchParams();
120-
for (_i = 0, _a = Object.entries(options.params); _i < _a.length; _i++) {
121-
_b = _a[_i], k = _b[0], v = _b[1];
116+
for (_i = 0, _b = Object.entries(options.params); _i < _b.length; _i++) {
117+
_c = _b[_i], k = _c[0], v = _c[1];
122118
if (v !== undefined && v !== null)
123119
search.append(k, String(v));
124120
}
@@ -137,17 +133,17 @@ var BaseClient = /** @class */ (function () {
137133
}
138134
}
139135
controller = new AbortController();
140-
timeoutId = setTimeout(function () { return controller.abort(); }, this.timeout);
136+
timeoutId = setTimeout(function () { return controller.abort(); }, requestTimeout);
141137
init.signal = controller.signal;
142-
_d.label = 3;
138+
_e.label = 3;
143139
case 3:
144-
_d.trys.push([3, 5, , 6]);
140+
_e.trys.push([3, 5, , 6]);
145141
return [4 /*yield*/, fetch(finalUrl, init)];
146142
case 4:
147-
response = _d.sent();
143+
response = _e.sent();
148144
return [3 /*break*/, 6];
149145
case 5:
150-
err_1 = _d.sent();
146+
err_1 = _e.sent();
151147
clearTimeout(timeoutId);
152148
wrapped = new Error((err_1 === null || err_1 === void 0 ? void 0 : err_1.message) || "Network request failed");
153149
wrapped.request = { url: finalUrl, method: method };
@@ -157,28 +153,28 @@ var BaseClient = /** @class */ (function () {
157153
clearTimeout(timeoutId);
158154
contentType = response.headers.get("content-type") || "";
159155
if (!contentType.includes("application/json")) return [3 /*break*/, 11];
160-
_d.label = 7;
156+
_e.label = 7;
161157
case 7:
162-
_d.trys.push([7, 9, , 10]);
158+
_e.trys.push([7, 9, , 10]);
163159
return [4 /*yield*/, response.json()];
164160
case 8:
165-
data = _d.sent();
161+
data = _e.sent();
166162
return [3 /*break*/, 10];
167163
case 9:
168-
_c = _d.sent();
164+
_d = _e.sent();
169165
data = null;
170166
return [3 /*break*/, 10];
171167
case 10: return [3 /*break*/, 13];
172168
case 11: return [4 /*yield*/, response.text()];
173169
case 12:
174-
text = _d.sent();
170+
text = _e.sent();
175171
try {
176172
data = JSON.parse(text);
177173
}
178-
catch (_e) {
174+
catch (_f) {
179175
data = text;
180176
}
181-
_d.label = 13;
177+
_e.label = 13;
182178
case 13:
183179
if (response.status < 200 || response.status >= 300) {
184180
headersObj_1 = {};
@@ -381,7 +377,7 @@ var BaseClient = /** @class */ (function () {
381377
});
382378
});
383379
};
384-
BaseClient.prototype.httpPUT = function (path, body) {
380+
BaseClient.prototype.httpPUT = function (path, body, options) {
385381
return __awaiter(this, void 0, void 0, function () {
386382
var response, error_4;
387383
return __generator(this, function (_a) {
@@ -393,6 +389,7 @@ var BaseClient = /** @class */ (function () {
393389
headers: {
394390
"x-api-key": this.key,
395391
},
392+
timeout: options === null || options === void 0 ? void 0 : options.timeout,
396393
})];
397394
case 1:
398395
response = _a.sent();

dist/client/resource.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ var Resource = /** @class */ (function (_super) {
8080
);
8181
return _super.prototype.httpPOST.call(this, this.path, requestBody);
8282
};
83-
Resource.prototype.put = function (id, body) {
83+
Resource.prototype.put = function (id, body, options) {
8484
this.path = this.formatPath(this.path, id);
85-
return _super.prototype.httpPUT.call(this, this.path, body);
85+
return _super.prototype.httpPUT.call(this, this.path, body, options);
8686
};
8787
return Resource;
8888
}(base_client_1.BaseClient));

dist/common/runtime.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.isCloudflareWorkers = void 0;
4+
/**
5+
* Detect if we're running in the Cloudflare Workers (workerd) runtime.
6+
* Workers sets navigator.userAgent to "Cloudflare-Workers" — this is the
7+
* documented and stable detection signal:
8+
* https://developers.cloudflare.com/workers/runtime-apis/web-standards/
9+
*/
10+
exports.isCloudflareWorkers = typeof navigator !== "undefined" &&
11+
navigator.userAgent === "Cloudflare-Workers";

dist/common/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ exports.RecordPerformanceOptions = zod_1.z.object({
180180
sample: zod_1.z.number().optional().default(0.2),
181181
overrideElapsed: zod_1.z.number().optional(),
182182
responseID: zod_1.z.string().optional(),
183+
timeout: zod_1.z.number().optional(),
183184
});
184185
// Mode constants
185186
exports.Modes = {

0 commit comments

Comments
 (0)