@hexclave/js 1.0.25 → 1.0.27
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/dist/dev-tool/dev-tool-core.js +1 -1
- package/dist/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +6 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +29 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +9 -8
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +44 -19
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +97 -8
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/index.d.ts +2 -1
- package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts +27 -0
- package/dist/esm/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
- package/dist/esm/lib/hexclave-app/plan-usage/index.js +1 -0
- package/dist/esm/lib/hexclave-app/projects/index.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/projects/index.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/projects/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +6 -0
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +29 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +9 -8
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js +43 -18
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +97 -8
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +2 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/hexclave-app/index.d.ts +2 -1
- package/dist/lib/hexclave-app/plan-usage/index.d.ts +27 -0
- package/dist/lib/hexclave-app/plan-usage/index.d.ts.map +1 -0
- package/dist/lib/hexclave-app/plan-usage/index.js +0 -0
- package/dist/lib/hexclave-app/projects/index.d.ts +1 -0
- package/dist/lib/hexclave-app/projects/index.d.ts.map +1 -1
- package/dist/lib/hexclave-app/projects/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dev-tool/dev-tool-core.ts +1 -1
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +34 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +9 -8
- package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +123 -4
- package/src/lib/hexclave-app/apps/implementations/session-replay.ts +63 -29
- package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +2 -0
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +1 -0
- package/src/lib/hexclave-app/index.ts +8 -0
- package/src/lib/hexclave-app/plan-usage/index.ts +29 -0
- package/src/lib/hexclave-app/projects/index.ts +1 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//===========================================
|
|
5
5
|
import { KnownErrors } from "@hexclave/shared/dist/known-errors";
|
|
6
6
|
import { isBrowserLike } from "@hexclave/shared/dist/utils/env";
|
|
7
|
-
import { captureWarning } from "@hexclave/shared/dist/utils/errors";
|
|
7
|
+
import { captureWarning, throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
8
8
|
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
9
9
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
10
10
|
|
|
@@ -110,6 +110,9 @@ const IDLE_TTL_MS = 3 * 60 * 1000;
|
|
|
110
110
|
const FLUSH_INTERVAL_MS = 5_000;
|
|
111
111
|
const MAX_EVENTS_PER_BATCH = 200;
|
|
112
112
|
const MAX_APPROX_BYTES_PER_BATCH = 512_000;
|
|
113
|
+
// The server rejects payloads > 1MB. Stay well under to account for JSON
|
|
114
|
+
// envelope overhead (browser_session_id, timestamps, wrapper keys, etc.).
|
|
115
|
+
const MAX_FLUSH_PAYLOAD_BYTES = 900_000;
|
|
113
116
|
|
|
114
117
|
export type StoredSession = {
|
|
115
118
|
session_id: string,
|
|
@@ -193,6 +196,7 @@ export class SessionRecorder {
|
|
|
193
196
|
private _detachListeners: (() => void) | null = null;
|
|
194
197
|
private _flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
195
198
|
private _events: unknown[] = [];
|
|
199
|
+
private _eventSizes: number[] = [];
|
|
196
200
|
private _approxBytes = 0;
|
|
197
201
|
private _lastPersistActivity = 0;
|
|
198
202
|
private _recording = false;
|
|
@@ -243,6 +247,7 @@ export class SessionRecorder {
|
|
|
243
247
|
|
|
244
248
|
clearBuffer() {
|
|
245
249
|
this._events = [];
|
|
250
|
+
this._eventSizes = [];
|
|
246
251
|
this._approxBytes = 0;
|
|
247
252
|
}
|
|
248
253
|
|
|
@@ -268,42 +273,68 @@ export class SessionRecorder {
|
|
|
268
273
|
const nowMs = Date.now();
|
|
269
274
|
const stored = getOrRotateSession({ key: this._storageKey, legacyKey: this._legacyStorageKey, nowMs });
|
|
270
275
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
started_at_ms: stored.created_at_ms,
|
|
277
|
-
sent_at_ms: nowMs,
|
|
278
|
-
events: this._events,
|
|
279
|
-
};
|
|
280
|
-
|
|
276
|
+
// Capture all buffered events upfront (before any await) so that
|
|
277
|
+
// stop() / _stopCurrentRecording() clearing this._events cannot race
|
|
278
|
+
// with the async send loop below and silently discard overflow batches.
|
|
279
|
+
const allEvents = this._events;
|
|
280
|
+
const allSizes = this._eventSizes;
|
|
281
281
|
this._events = [];
|
|
282
|
+
this._eventSizes = [];
|
|
282
283
|
this._approxBytes = 0;
|
|
283
284
|
|
|
284
285
|
this._flushInProgress = true;
|
|
285
286
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
287
|
+
let offset = 0;
|
|
288
|
+
while (offset < allEvents.length) {
|
|
289
|
+
// Build a batch that fits under the server's payload limit.
|
|
290
|
+
// When _flushInProgress blocked earlier flushes, events can accumulate
|
|
291
|
+
// well past MAX_APPROX_BYTES_PER_BATCH; sending them all at once would
|
|
292
|
+
// exceed the server's 1MB body limit (413).
|
|
293
|
+
let batchBytes = 0;
|
|
294
|
+
let batchEnd = offset;
|
|
295
|
+
for (let i = offset; i < allEvents.length; i++) {
|
|
296
|
+
const nextSize = allSizes[i] ?? throwErr("_eventSizes out of sync with _events — this should never happen");
|
|
297
|
+
if (batchBytes + nextSize > MAX_FLUSH_PAYLOAD_BYTES && batchEnd > offset) break;
|
|
298
|
+
batchBytes += nextSize;
|
|
299
|
+
batchEnd = i + 1;
|
|
295
300
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
|
|
302
|
+
const batchEvents = allEvents.slice(offset, batchEnd);
|
|
303
|
+
offset = batchEnd;
|
|
304
|
+
|
|
305
|
+
const batchId = generateUuid();
|
|
306
|
+
const payload = {
|
|
307
|
+
browser_session_id: stored.session_id,
|
|
308
|
+
session_replay_segment_id: this._sessionReplaySegmentId,
|
|
309
|
+
batch_id: batchId,
|
|
310
|
+
started_at_ms: stored.created_at_ms,
|
|
311
|
+
sent_at_ms: nowMs,
|
|
312
|
+
events: batchEvents,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const res = await this._deps.sendBatch(
|
|
316
|
+
JSON.stringify(payload),
|
|
317
|
+
{ keepalive: options.keepalive },
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (res.status === "error") {
|
|
321
|
+
if (isAnalyticsNotEnabledError(res.error)) {
|
|
322
|
+
this._disable();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Ad blockers commonly block analytics endpoints, causing network
|
|
326
|
+
// errors. These are expected and should not pollute the console.
|
|
327
|
+
if (isAdBlockerNetworkError(res.error)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
captureWarning("SessionRecorder.flush", res.error);
|
|
299
331
|
return;
|
|
300
332
|
}
|
|
301
|
-
captureWarning("SessionRecorder.flush", res.error);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
333
|
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
if (!res.data.ok) {
|
|
335
|
+
captureWarning("SessionRecorder.flush", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
307
338
|
}
|
|
308
339
|
} finally {
|
|
309
340
|
this._flushInProgress = false;
|
|
@@ -357,8 +388,10 @@ export class SessionRecorder {
|
|
|
357
388
|
}
|
|
358
389
|
}
|
|
359
390
|
|
|
391
|
+
const eventSize = JSON.stringify(event).length;
|
|
360
392
|
this._events.push(event);
|
|
361
|
-
this.
|
|
393
|
+
this._eventSizes.push(eventSize);
|
|
394
|
+
this._approxBytes += eventSize;
|
|
362
395
|
if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {
|
|
363
396
|
runAsynchronously(() => this._flush({ keepalive: false }));
|
|
364
397
|
}
|
|
@@ -391,6 +424,7 @@ export class SessionRecorder {
|
|
|
391
424
|
this._stopRecording = null;
|
|
392
425
|
}
|
|
393
426
|
this._events = [];
|
|
427
|
+
this._eventSizes = [];
|
|
394
428
|
this._approxBytes = 0;
|
|
395
429
|
this._recording = false;
|
|
396
430
|
}
|
|
@@ -13,6 +13,7 @@ import { AsyncStoreProperty, EmailConfig } from "../../common";
|
|
|
13
13
|
import { AdminEmailOutbox, AdminSentEmail } from "../../email";
|
|
14
14
|
import { InternalApiKey, InternalApiKeyCreateOptions, InternalApiKeyFirstView } from "../../internal-api-keys";
|
|
15
15
|
import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions } from "../../permissions";
|
|
16
|
+
import type { PlanUsage } from "../../plan-usage";
|
|
16
17
|
import { AdminProject } from "../../projects";
|
|
17
18
|
import { _HexclaveAdminAppImpl } from "../implementations";
|
|
18
19
|
import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
|
|
@@ -75,6 +76,7 @@ export type StackAdminAppConstructorOptions<HasTokenStore extends boolean, Proje
|
|
|
75
76
|
/** @deprecated Use `HexclaveAdminApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
|
|
76
77
|
export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (
|
|
77
78
|
& AsyncStoreProperty<"project", [], AdminProject, false>
|
|
79
|
+
& AsyncStoreProperty<"planUsage", [], PlanUsage, false>
|
|
78
80
|
& AsyncStoreProperty<"internalApiKeys", [], InternalApiKey[], true>
|
|
79
81
|
& AsyncStoreProperty<"teamPermissionDefinitions", [], AdminTeamPermissionDefinition[], true>
|
|
80
82
|
& AsyncStoreProperty<"projectPermissionDefinitions", [], AdminProjectPermissionDefinition[], true>
|
|
@@ -123,6 +123,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
|
|
123
123
|
sendAnalyticsEventBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,
|
|
124
124
|
addRequestListener(listener: RequestListener): () => void,
|
|
125
125
|
sendRequest(path: string, requestOptions: RequestInit, requestType?: "client" | "server" | "admin"): Promise<Response>,
|
|
126
|
+
getUrls(): Readonly<ResolvedHandlerUrls>,
|
|
126
127
|
getRedirectMethod(): RedirectMethod,
|
|
127
128
|
redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,
|
|
128
129
|
redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,
|
|
@@ -118,6 +118,14 @@ export type {
|
|
|
118
118
|
PushedConfigSource
|
|
119
119
|
} from "./projects";
|
|
120
120
|
|
|
121
|
+
export type {
|
|
122
|
+
PlanUsage,
|
|
123
|
+
PlanUsageKind,
|
|
124
|
+
PlanUsageNextPlanId,
|
|
125
|
+
PlanUsagePlanId,
|
|
126
|
+
PlanUsageRow,
|
|
127
|
+
} from "./plan-usage";
|
|
128
|
+
|
|
121
129
|
export type {
|
|
122
130
|
EditableTeamMemberProfile, ReceivedTeamInvitation,
|
|
123
131
|
SentTeamInvitation, ServerListUsersOptions,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
export type PlanUsageKind = "current" | "metered" | "capability";
|
|
6
|
+
export type PlanUsagePlanId = "free" | "team" | "growth";
|
|
7
|
+
export type PlanUsageNextPlanId = "team" | "growth";
|
|
8
|
+
|
|
9
|
+
export type PlanUsageRow = {
|
|
10
|
+
itemId: string,
|
|
11
|
+
displayName: string,
|
|
12
|
+
kind: PlanUsageKind,
|
|
13
|
+
used: number | null,
|
|
14
|
+
limit: number | null,
|
|
15
|
+
remaining: number | null,
|
|
16
|
+
overage: number | null,
|
|
17
|
+
isUnlimited: boolean,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PlanUsage = {
|
|
21
|
+
ownerTeamId: string,
|
|
22
|
+
ownerTeamDisplayName: string,
|
|
23
|
+
planId: PlanUsagePlanId,
|
|
24
|
+
planDisplayName: string,
|
|
25
|
+
periodStart: Date,
|
|
26
|
+
periodEnd: Date,
|
|
27
|
+
nextPlanId: PlanUsageNextPlanId | null,
|
|
28
|
+
rows: PlanUsageRow[],
|
|
29
|
+
};
|
|
@@ -44,6 +44,7 @@ export type AdminProject = {
|
|
|
44
44
|
readonly isDevelopmentEnvironment: boolean,
|
|
45
45
|
readonly ownerTeamId: string | null,
|
|
46
46
|
readonly onboardingStatus: ProjectOnboardingStatus,
|
|
47
|
+
readonly onboardingState: NonNullable<ProjectsCrud["Admin"]["Read"]["onboarding_state"]> | null,
|
|
47
48
|
readonly logoUrl: string | null | undefined,
|
|
48
49
|
readonly logoFullUrl: string | null | undefined,
|
|
49
50
|
readonly logoDarkModeUrl: string | null | undefined,
|