@shipeasy/sdk 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +37 -3
- package/dist/client/index.mjs +37 -3
- package/dist/server/index.d.mts +55 -25
- package/dist/server/index.d.ts +55 -25
- package/dist/server/index.js +52 -37
- package/dist/server/index.mjs +50 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,47 @@ const client = new FlagsClient({ sdkKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
|
53
53
|
const cfg = await client.getConfig("plan_limits", { user_id: "u-1" });
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## SSR bootstrap (flags on first paint)
|
|
57
|
+
|
|
58
|
+
Server-render the evaluated flags / configs / experiments into the page so the
|
|
59
|
+
browser SDK reads them **synchronously on first paint** — no flash, no extra
|
|
60
|
+
round-trip. The `shipeasy()` server handle emits two declarative `<script>`
|
|
61
|
+
tags. **No SDK key is embedded** in the bootstrap tag (the server key never
|
|
62
|
+
reaches the browser).
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// app/layout.tsx — Next.js root layout (React Server Component)
|
|
66
|
+
import { shipeasy } from "@shipeasy/sdk/server";
|
|
67
|
+
|
|
68
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
69
|
+
const se = await shipeasy({ serverKey: process.env.SHIPEASY_SERVER_KEY ?? "" });
|
|
70
|
+
const boot = se.getBootstrapData({
|
|
71
|
+
// public client key — lets the i18n loader revalidate strings at runtime
|
|
72
|
+
clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY,
|
|
73
|
+
});
|
|
74
|
+
return (
|
|
75
|
+
<html>
|
|
76
|
+
<body>
|
|
77
|
+
{/* Render REAL <script> elements — scripts set via
|
|
78
|
+
dangerouslySetInnerHTML do NOT execute. */}
|
|
79
|
+
<script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
|
|
80
|
+
{boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
|
|
81
|
+
{children}
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`bootstrap.src` is `https://cdn.shipeasy.ai/sdk/bootstrap.js` — a static loader
|
|
89
|
+
that reads its own `data-*` attributes, hydrates `window.__SE_BOOTSTRAP`, and
|
|
90
|
+
persists the `__se_anon_id` cookie so the browser buckets **identically** to the
|
|
91
|
+
server. The i18n loader tag carries the SSR strings (`data-strings`) for a
|
|
92
|
+
no-flash first paint plus the public client key for runtime revalidation.
|
|
93
|
+
|
|
94
|
+
For non-React SSR (Express, raw templates), `se.getBootstrapTags()` returns the
|
|
95
|
+
same two tags as an HTML string you can drop straight into the served markup.
|
|
96
|
+
|
|
56
97
|
## Error tracking — `see()`
|
|
57
98
|
|
|
58
99
|
`see` (shipeasy error) is the structured error reporter — opes-style: every
|
package/dist/client/index.d.mts
CHANGED
|
@@ -530,8 +530,8 @@ interface BootstrapPayload {
|
|
|
530
530
|
/** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
|
|
531
531
|
i18nProfile?: string;
|
|
532
532
|
apiUrl?: string;
|
|
533
|
-
/**
|
|
534
|
-
|
|
533
|
+
/** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
|
|
534
|
+
anonId?: string;
|
|
535
535
|
}
|
|
536
536
|
/**
|
|
537
537
|
* Universal flags facade. Methods return safe defaults when the singleton
|
package/dist/client/index.d.ts
CHANGED
|
@@ -530,8 +530,8 @@ interface BootstrapPayload {
|
|
|
530
530
|
/** i18n profile the server rendered with, so the client loader matches. No key is embedded. */
|
|
531
531
|
i18nProfile?: string;
|
|
532
532
|
apiUrl?: string;
|
|
533
|
-
/**
|
|
534
|
-
|
|
533
|
+
/** Stable anonymous bucketing id the server evaluated against (cross-SDK contract). */
|
|
534
|
+
anonId?: string;
|
|
535
535
|
}
|
|
536
536
|
/**
|
|
537
537
|
* Universal flags facade. Methods return safe defaults when the singleton
|
package/dist/client/index.js
CHANGED
|
@@ -768,7 +768,7 @@ function writeStickyCookie(map) {
|
|
|
768
768
|
function getOrCreateAnonId() {
|
|
769
769
|
let id = readAnonCookie();
|
|
770
770
|
if (!id && typeof window !== "undefined") {
|
|
771
|
-
id =
|
|
771
|
+
id = getBootstrap()?.anonId ?? null;
|
|
772
772
|
}
|
|
773
773
|
if (!id) {
|
|
774
774
|
try {
|
|
@@ -1430,6 +1430,13 @@ var _i18nLoaderInjected = false;
|
|
|
1430
1430
|
function injectI18nLoader(clientKey, baseUrl, profileOpt) {
|
|
1431
1431
|
if (_i18nLoaderInjected || typeof document === "undefined") return;
|
|
1432
1432
|
if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
|
|
1433
|
+
try {
|
|
1434
|
+
if (document.querySelector?.('script[src*="/sdk/i18n/loader.js"][data-key]')) {
|
|
1435
|
+
_i18nLoaderInjected = true;
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1433
1440
|
_i18nLoaderInjected = true;
|
|
1434
1441
|
try {
|
|
1435
1442
|
const bs = getBootstrap();
|
|
@@ -1442,9 +1449,36 @@ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
|
|
|
1442
1449
|
} catch {
|
|
1443
1450
|
}
|
|
1444
1451
|
}
|
|
1452
|
+
var _bootstrapFromTag = null;
|
|
1453
|
+
function readBootstrapTag() {
|
|
1454
|
+
if (_bootstrapFromTag) return _bootstrapFromTag;
|
|
1455
|
+
if (typeof document === "undefined" || typeof document.querySelector !== "function") return null;
|
|
1456
|
+
const el = document.querySelector("script[data-se-bootstrap]");
|
|
1457
|
+
if (!el) return null;
|
|
1458
|
+
const J = (name) => {
|
|
1459
|
+
try {
|
|
1460
|
+
return JSON.parse(el.getAttribute(name) || "{}");
|
|
1461
|
+
} catch {
|
|
1462
|
+
return {};
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const bs = {
|
|
1466
|
+
flags: J("data-flags"),
|
|
1467
|
+
configs: J("data-configs"),
|
|
1468
|
+
experiments: J("data-experiments"),
|
|
1469
|
+
killswitches: J("data-killswitches"),
|
|
1470
|
+
i18nProfile: el.getAttribute("data-i18n-profile") || void 0,
|
|
1471
|
+
apiUrl: el.getAttribute("data-api-url") || void 0,
|
|
1472
|
+
anonId: el.getAttribute("data-anon-id") || void 0
|
|
1473
|
+
};
|
|
1474
|
+
_bootstrapFromTag = bs;
|
|
1475
|
+
return bs;
|
|
1476
|
+
}
|
|
1445
1477
|
function getBootstrap() {
|
|
1446
1478
|
if (typeof window === "undefined") return null;
|
|
1447
|
-
|
|
1479
|
+
const g = window.__SE_BOOTSTRAP;
|
|
1480
|
+
if (g) return g;
|
|
1481
|
+
return readBootstrapTag();
|
|
1448
1482
|
}
|
|
1449
1483
|
var _mountedAndReady = false;
|
|
1450
1484
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
@@ -1623,7 +1657,7 @@ function getSSRI18nStore() {
|
|
|
1623
1657
|
var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
|
|
1624
1658
|
function isEditLabelsMode() {
|
|
1625
1659
|
if (typeof window !== "undefined") {
|
|
1626
|
-
return
|
|
1660
|
+
return new URLSearchParams(location.search).has("se_edit_labels") || /(?:^|;\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);
|
|
1627
1661
|
}
|
|
1628
1662
|
const val = globalThis[_EDIT_MODE_SSR_SYM];
|
|
1629
1663
|
if (typeof val === "boolean") return val;
|
package/dist/client/index.mjs
CHANGED
|
@@ -719,7 +719,7 @@ function writeStickyCookie(map) {
|
|
|
719
719
|
function getOrCreateAnonId() {
|
|
720
720
|
let id = readAnonCookie();
|
|
721
721
|
if (!id && typeof window !== "undefined") {
|
|
722
|
-
id =
|
|
722
|
+
id = getBootstrap()?.anonId ?? null;
|
|
723
723
|
}
|
|
724
724
|
if (!id) {
|
|
725
725
|
try {
|
|
@@ -1381,6 +1381,13 @@ var _i18nLoaderInjected = false;
|
|
|
1381
1381
|
function injectI18nLoader(clientKey, baseUrl, profileOpt) {
|
|
1382
1382
|
if (_i18nLoaderInjected || typeof document === "undefined") return;
|
|
1383
1383
|
if (!clientKey || typeof document.createElement !== "function" || !document.head) return;
|
|
1384
|
+
try {
|
|
1385
|
+
if (document.querySelector?.('script[src*="/sdk/i18n/loader.js"][data-key]')) {
|
|
1386
|
+
_i18nLoaderInjected = true;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
} catch {
|
|
1390
|
+
}
|
|
1384
1391
|
_i18nLoaderInjected = true;
|
|
1385
1392
|
try {
|
|
1386
1393
|
const bs = getBootstrap();
|
|
@@ -1393,9 +1400,36 @@ function injectI18nLoader(clientKey, baseUrl, profileOpt) {
|
|
|
1393
1400
|
} catch {
|
|
1394
1401
|
}
|
|
1395
1402
|
}
|
|
1403
|
+
var _bootstrapFromTag = null;
|
|
1404
|
+
function readBootstrapTag() {
|
|
1405
|
+
if (_bootstrapFromTag) return _bootstrapFromTag;
|
|
1406
|
+
if (typeof document === "undefined" || typeof document.querySelector !== "function") return null;
|
|
1407
|
+
const el = document.querySelector("script[data-se-bootstrap]");
|
|
1408
|
+
if (!el) return null;
|
|
1409
|
+
const J = (name) => {
|
|
1410
|
+
try {
|
|
1411
|
+
return JSON.parse(el.getAttribute(name) || "{}");
|
|
1412
|
+
} catch {
|
|
1413
|
+
return {};
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
const bs = {
|
|
1417
|
+
flags: J("data-flags"),
|
|
1418
|
+
configs: J("data-configs"),
|
|
1419
|
+
experiments: J("data-experiments"),
|
|
1420
|
+
killswitches: J("data-killswitches"),
|
|
1421
|
+
i18nProfile: el.getAttribute("data-i18n-profile") || void 0,
|
|
1422
|
+
apiUrl: el.getAttribute("data-api-url") || void 0,
|
|
1423
|
+
anonId: el.getAttribute("data-anon-id") || void 0
|
|
1424
|
+
};
|
|
1425
|
+
_bootstrapFromTag = bs;
|
|
1426
|
+
return bs;
|
|
1427
|
+
}
|
|
1396
1428
|
function getBootstrap() {
|
|
1397
1429
|
if (typeof window === "undefined") return null;
|
|
1398
|
-
|
|
1430
|
+
const g = window.__SE_BOOTSTRAP;
|
|
1431
|
+
if (g) return g;
|
|
1432
|
+
return readBootstrapTag();
|
|
1399
1433
|
}
|
|
1400
1434
|
var _mountedAndReady = false;
|
|
1401
1435
|
var _standaloneListeners = /* @__PURE__ */ new Set();
|
|
@@ -1574,7 +1608,7 @@ function getSSRI18nStore() {
|
|
|
1574
1608
|
var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
|
|
1575
1609
|
function isEditLabelsMode() {
|
|
1576
1610
|
if (typeof window !== "undefined") {
|
|
1577
|
-
return
|
|
1611
|
+
return new URLSearchParams(location.search).has("se_edit_labels") || /(?:^|;\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);
|
|
1578
1612
|
}
|
|
1579
1613
|
const val = globalThis[_EDIT_MODE_SSR_SYM];
|
|
1580
1614
|
if (typeof val === "boolean") return val;
|
package/dist/server/index.d.mts
CHANGED
|
@@ -477,8 +477,22 @@ interface ShipeasyServerHandle {
|
|
|
477
477
|
flags: Record<string, boolean>;
|
|
478
478
|
configs: Record<string, unknown>;
|
|
479
479
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
480
|
-
/**
|
|
481
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Structured `<script>` tag specs to drop into the document head: the
|
|
482
|
+
* cross-platform `se-bootstrap.js` tag (hydrates window.__SE_BOOTSTRAP +
|
|
483
|
+
* writes the anon cookie) and, when there are SSR strings or a client key,
|
|
484
|
+
* the i18n loader tag. Use this in React: scripts inserted via
|
|
485
|
+
* `dangerouslySetInnerHTML` do NOT execute, so render real `<script>`
|
|
486
|
+
* elements from these specs (see apps/ui root layout).
|
|
487
|
+
*/
|
|
488
|
+
getBootstrapData(emit?: BootstrapEmitOptions): BootstrapData;
|
|
489
|
+
/**
|
|
490
|
+
* The same tags rendered as an HTML string, for non-React SSR (Express, raw
|
|
491
|
+
* templates) where the markup is emitted directly into the served HTML (and
|
|
492
|
+
* therefore executes normally). This is the canonical cross-platform shape
|
|
493
|
+
* every server SDK mirrors.
|
|
494
|
+
*/
|
|
495
|
+
getBootstrapTags(emit?: BootstrapEmitOptions): string;
|
|
482
496
|
}
|
|
483
497
|
/**
|
|
484
498
|
* Initialise the ShipEasy server SDK, evaluate flags for this request, and
|
|
@@ -487,35 +501,51 @@ interface ShipeasyServerHandle {
|
|
|
487
501
|
* payloads and i18n falls back to hardcoded strings.
|
|
488
502
|
*/
|
|
489
503
|
declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
|
|
490
|
-
interface
|
|
491
|
-
/** i18n profile recorded
|
|
504
|
+
interface BootstrapEmitOptions {
|
|
505
|
+
/** i18n profile recorded on both tags so the client loader matches SSR. Defaults to "en:prod". */
|
|
492
506
|
i18nProfile?: string;
|
|
493
|
-
/** When true, tEl() embeds label markers so the devtools can highlight them. */
|
|
494
|
-
editLabels?: boolean;
|
|
495
507
|
/**
|
|
496
|
-
* Stable anonymous bucketing id the server evaluated against. Emitted
|
|
497
|
-
*
|
|
498
|
-
* `__se_anon_id` cookie, so the
|
|
499
|
-
*
|
|
500
|
-
* middleware doesn't cover. See
|
|
508
|
+
* Stable anonymous bucketing id the server evaluated against. Emitted as
|
|
509
|
+
* `data-anon-id`; se-bootstrap.js exposes it on window.__SE_BOOTSTRAP and
|
|
510
|
+
* persists it (pre-paint) to the first-party `__se_anon_id` cookie, so the
|
|
511
|
+
* browser SDK buckets identically to SSR. Normally minted by edge
|
|
512
|
+
* middleware; this is the fallback for routes it doesn't cover. See
|
|
513
|
+
* experiment-platform/18-identity-bucketing.md.
|
|
501
514
|
*/
|
|
502
515
|
anonId?: string;
|
|
516
|
+
/**
|
|
517
|
+
* Public client key. When provided, the i18n loader tag can revalidate
|
|
518
|
+
* strings at runtime (`data-key`). Optional — the bootstrap tag NEVER carries
|
|
519
|
+
* a key, and SSR first paint works keyless via `data-strings`.
|
|
520
|
+
*/
|
|
521
|
+
clientKey?: string;
|
|
522
|
+
/** CDN base for the tag `src`s. Defaults to https://cdn.shipeasy.ai. */
|
|
523
|
+
baseUrl?: string;
|
|
524
|
+
}
|
|
525
|
+
interface ScriptTagSpec {
|
|
526
|
+
src: string;
|
|
527
|
+
/** Attribute name → value. An empty-string value renders as a bare boolean attribute (e.g. `data-se-bootstrap`). */
|
|
528
|
+
attrs: Record<string, string>;
|
|
529
|
+
}
|
|
530
|
+
interface BootstrapData {
|
|
531
|
+
/** se-bootstrap.js tag — always present. */
|
|
532
|
+
bootstrap: ScriptTagSpec;
|
|
533
|
+
/** i18n loader tag — null when there are no SSR strings and no client key. */
|
|
534
|
+
i18nLoader: ScriptTagSpec | null;
|
|
503
535
|
}
|
|
504
536
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* Framework-agnostic: set innerHTML on a <script> element, nothing else required.
|
|
537
|
+
* Build the structured `<script>` tag specs for the SSR bootstrap. Render
|
|
538
|
+
* these as REAL `<script>` elements (React: scripts set via innerHTML do not
|
|
539
|
+
* execute). No SDK key is ever embedded in the bootstrap tag.
|
|
540
|
+
*/
|
|
541
|
+
declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): BootstrapData;
|
|
542
|
+
/**
|
|
543
|
+
* The bootstrap (and optional i18n loader) tags as an HTML string, for non-React
|
|
544
|
+
* SSR (Express, raw templates) where the markup is emitted directly into the
|
|
545
|
+
* served HTML and executes normally. React callers should use
|
|
546
|
+
* {@link getBootstrapData} and render real `<script>` elements instead.
|
|
517
547
|
*/
|
|
518
|
-
declare function
|
|
548
|
+
declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
|
|
519
549
|
declare const flags: {
|
|
520
550
|
configure(opts: FlagsClientOptions): void;
|
|
521
551
|
/**
|
|
@@ -600,4 +630,4 @@ interface SeeApi {
|
|
|
600
630
|
*/
|
|
601
631
|
declare const see: SeeApi;
|
|
602
632
|
|
|
603
|
-
export { ANON_ID_COOKIE, type
|
|
633
|
+
export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -477,8 +477,22 @@ interface ShipeasyServerHandle {
|
|
|
477
477
|
flags: Record<string, boolean>;
|
|
478
478
|
configs: Record<string, unknown>;
|
|
479
479
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
480
|
-
/**
|
|
481
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Structured `<script>` tag specs to drop into the document head: the
|
|
482
|
+
* cross-platform `se-bootstrap.js` tag (hydrates window.__SE_BOOTSTRAP +
|
|
483
|
+
* writes the anon cookie) and, when there are SSR strings or a client key,
|
|
484
|
+
* the i18n loader tag. Use this in React: scripts inserted via
|
|
485
|
+
* `dangerouslySetInnerHTML` do NOT execute, so render real `<script>`
|
|
486
|
+
* elements from these specs (see apps/ui root layout).
|
|
487
|
+
*/
|
|
488
|
+
getBootstrapData(emit?: BootstrapEmitOptions): BootstrapData;
|
|
489
|
+
/**
|
|
490
|
+
* The same tags rendered as an HTML string, for non-React SSR (Express, raw
|
|
491
|
+
* templates) where the markup is emitted directly into the served HTML (and
|
|
492
|
+
* therefore executes normally). This is the canonical cross-platform shape
|
|
493
|
+
* every server SDK mirrors.
|
|
494
|
+
*/
|
|
495
|
+
getBootstrapTags(emit?: BootstrapEmitOptions): string;
|
|
482
496
|
}
|
|
483
497
|
/**
|
|
484
498
|
* Initialise the ShipEasy server SDK, evaluate flags for this request, and
|
|
@@ -487,35 +501,51 @@ interface ShipeasyServerHandle {
|
|
|
487
501
|
* payloads and i18n falls back to hardcoded strings.
|
|
488
502
|
*/
|
|
489
503
|
declare function shipeasy(opts: ShipeasyServerConfig): Promise<ShipeasyServerHandle>;
|
|
490
|
-
interface
|
|
491
|
-
/** i18n profile recorded
|
|
504
|
+
interface BootstrapEmitOptions {
|
|
505
|
+
/** i18n profile recorded on both tags so the client loader matches SSR. Defaults to "en:prod". */
|
|
492
506
|
i18nProfile?: string;
|
|
493
|
-
/** When true, tEl() embeds label markers so the devtools can highlight them. */
|
|
494
|
-
editLabels?: boolean;
|
|
495
507
|
/**
|
|
496
|
-
* Stable anonymous bucketing id the server evaluated against. Emitted
|
|
497
|
-
*
|
|
498
|
-
* `__se_anon_id` cookie, so the
|
|
499
|
-
*
|
|
500
|
-
* middleware doesn't cover. See
|
|
508
|
+
* Stable anonymous bucketing id the server evaluated against. Emitted as
|
|
509
|
+
* `data-anon-id`; se-bootstrap.js exposes it on window.__SE_BOOTSTRAP and
|
|
510
|
+
* persists it (pre-paint) to the first-party `__se_anon_id` cookie, so the
|
|
511
|
+
* browser SDK buckets identically to SSR. Normally minted by edge
|
|
512
|
+
* middleware; this is the fallback for routes it doesn't cover. See
|
|
513
|
+
* experiment-platform/18-identity-bucketing.md.
|
|
501
514
|
*/
|
|
502
515
|
anonId?: string;
|
|
516
|
+
/**
|
|
517
|
+
* Public client key. When provided, the i18n loader tag can revalidate
|
|
518
|
+
* strings at runtime (`data-key`). Optional — the bootstrap tag NEVER carries
|
|
519
|
+
* a key, and SSR first paint works keyless via `data-strings`.
|
|
520
|
+
*/
|
|
521
|
+
clientKey?: string;
|
|
522
|
+
/** CDN base for the tag `src`s. Defaults to https://cdn.shipeasy.ai. */
|
|
523
|
+
baseUrl?: string;
|
|
524
|
+
}
|
|
525
|
+
interface ScriptTagSpec {
|
|
526
|
+
src: string;
|
|
527
|
+
/** Attribute name → value. An empty-string value renders as a bare boolean attribute (e.g. `data-se-bootstrap`). */
|
|
528
|
+
attrs: Record<string, string>;
|
|
529
|
+
}
|
|
530
|
+
interface BootstrapData {
|
|
531
|
+
/** se-bootstrap.js tag — always present. */
|
|
532
|
+
bootstrap: ScriptTagSpec;
|
|
533
|
+
/** i18n loader tag — null when there are no SSR strings and no client key. */
|
|
534
|
+
i18nLoader: ScriptTagSpec | null;
|
|
503
535
|
}
|
|
504
536
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* Framework-agnostic: set innerHTML on a <script> element, nothing else required.
|
|
537
|
+
* Build the structured `<script>` tag specs for the SSR bootstrap. Render
|
|
538
|
+
* these as REAL `<script>` elements (React: scripts set via innerHTML do not
|
|
539
|
+
* execute). No SDK key is ever embedded in the bootstrap tag.
|
|
540
|
+
*/
|
|
541
|
+
declare function getBootstrapData(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): BootstrapData;
|
|
542
|
+
/**
|
|
543
|
+
* The bootstrap (and optional i18n loader) tags as an HTML string, for non-React
|
|
544
|
+
* SSR (Express, raw templates) where the markup is emitted directly into the
|
|
545
|
+
* served HTML and executes normally. React callers should use
|
|
546
|
+
* {@link getBootstrapData} and render real `<script>` elements instead.
|
|
517
547
|
*/
|
|
518
|
-
declare function
|
|
548
|
+
declare function getBootstrapTags(bootstrap: BootstrapPayload | null, i18nData: I18nForRequest | null, opts: BootstrapEmitOptions): string;
|
|
519
549
|
declare const flags: {
|
|
520
550
|
configure(opts: FlagsClientOptions): void;
|
|
521
551
|
/**
|
|
@@ -600,4 +630,4 @@ interface SeeApi {
|
|
|
600
630
|
*/
|
|
601
631
|
declare const see: SeeApi;
|
|
602
632
|
|
|
603
|
-
export { ANON_ID_COOKIE, type
|
|
633
|
+
export { ANON_ID_COOKIE, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetShipeasyServerForTests, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
|
package/dist/server/index.js
CHANGED
|
@@ -39,7 +39,8 @@ __export(server_exports, {
|
|
|
39
39
|
createInMemoryStickyStore: () => createInMemoryStickyStore,
|
|
40
40
|
fetchLabelsForSSR: () => fetchLabelsForSSR,
|
|
41
41
|
flags: () => flags,
|
|
42
|
-
|
|
42
|
+
getBootstrapData: () => getBootstrapData,
|
|
43
|
+
getBootstrapTags: () => getBootstrapTags,
|
|
43
44
|
getShipeasyServerClient: () => getShipeasyServerClient,
|
|
44
45
|
i18n: () => i18n,
|
|
45
46
|
isExpected: () => isExpected,
|
|
@@ -1161,49 +1162,62 @@ async function shipeasy(opts) {
|
|
|
1161
1162
|
flags: bootstrap.flags,
|
|
1162
1163
|
configs: bootstrap.configs,
|
|
1163
1164
|
experiments: bootstrap.experiments,
|
|
1164
|
-
|
|
1165
|
-
return
|
|
1166
|
-
editLabels,
|
|
1165
|
+
getBootstrapData(emit) {
|
|
1166
|
+
return getBootstrapData(bootstrap, i18nData, {
|
|
1167
1167
|
i18nProfile: profile,
|
|
1168
|
-
anonId
|
|
1168
|
+
anonId,
|
|
1169
|
+
...emit
|
|
1170
|
+
});
|
|
1171
|
+
},
|
|
1172
|
+
getBootstrapTags(emit) {
|
|
1173
|
+
return getBootstrapTags(bootstrap, i18nData, {
|
|
1174
|
+
i18nProfile: profile,
|
|
1175
|
+
anonId,
|
|
1176
|
+
...emit
|
|
1169
1177
|
});
|
|
1170
1178
|
}
|
|
1171
1179
|
};
|
|
1172
1180
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1181
|
+
var DEFAULT_CDN = "https://cdn.shipeasy.ai";
|
|
1182
|
+
function getBootstrapData(bootstrap, i18nData, opts) {
|
|
1183
|
+
const base = (opts.baseUrl ?? DEFAULT_CDN).replace(/\/$/, "");
|
|
1176
1184
|
const profile = opts.i18nProfile ?? "en:prod";
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
+
const attrs = {
|
|
1186
|
+
"data-se-bootstrap": "",
|
|
1187
|
+
"data-flags": JSON.stringify(bootstrap?.flags ?? {}),
|
|
1188
|
+
"data-configs": JSON.stringify(bootstrap?.configs ?? {}),
|
|
1189
|
+
"data-experiments": JSON.stringify(bootstrap?.experiments ?? {}),
|
|
1190
|
+
"data-killswitches": JSON.stringify(bootstrap?.killswitches ?? {}),
|
|
1191
|
+
"data-i18n-profile": profile,
|
|
1192
|
+
"data-api-url": base
|
|
1185
1193
|
};
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
|
|
1199
|
-
parts.push(
|
|
1200
|
-
`(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
|
|
1201
|
-
);
|
|
1194
|
+
if (opts.anonId) attrs["data-anon-id"] = opts.anonId;
|
|
1195
|
+
const bootstrapTag = { src: `${base}/sdk/bootstrap.js`, attrs };
|
|
1196
|
+
let i18nLoader = null;
|
|
1197
|
+
const hasStrings = !!(i18nData?.strings && Object.keys(i18nData.strings).length > 0);
|
|
1198
|
+
if (hasStrings || opts.clientKey) {
|
|
1199
|
+
const i18nAttrs = { "data-profile": profile };
|
|
1200
|
+
if (opts.clientKey) i18nAttrs["data-key"] = opts.clientKey;
|
|
1201
|
+
if (hasStrings) {
|
|
1202
|
+
i18nAttrs["data-strings"] = JSON.stringify(i18nData.strings);
|
|
1203
|
+
i18nAttrs["data-locale"] = i18nData.locale;
|
|
1204
|
+
}
|
|
1205
|
+
i18nLoader = { src: `${base}/sdk/i18n/loader.js`, attrs: i18nAttrs };
|
|
1202
1206
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
return
|
|
1207
|
+
return { bootstrap: bootstrapTag, i18nLoader };
|
|
1208
|
+
}
|
|
1209
|
+
function escapeAttr(v) {
|
|
1210
|
+
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1211
|
+
}
|
|
1212
|
+
function renderScriptTag(spec) {
|
|
1213
|
+
const attrs = Object.entries(spec.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
|
|
1214
|
+
return `<script src="${escapeAttr(spec.src)}"${attrs}></script>`;
|
|
1215
|
+
}
|
|
1216
|
+
function getBootstrapTags(bootstrap, i18nData, opts) {
|
|
1217
|
+
const data = getBootstrapData(bootstrap, i18nData, opts);
|
|
1218
|
+
const tags = [data.bootstrap];
|
|
1219
|
+
if (data.i18nLoader) tags.push(data.i18nLoader);
|
|
1220
|
+
return tags.map(renderScriptTag).join("");
|
|
1207
1221
|
}
|
|
1208
1222
|
var flags = {
|
|
1209
1223
|
configure(opts) {
|
|
@@ -1298,7 +1312,8 @@ var see = Object.assign(
|
|
|
1298
1312
|
createInMemoryStickyStore,
|
|
1299
1313
|
fetchLabelsForSSR,
|
|
1300
1314
|
flags,
|
|
1301
|
-
|
|
1315
|
+
getBootstrapData,
|
|
1316
|
+
getBootstrapTags,
|
|
1302
1317
|
getShipeasyServerClient,
|
|
1303
1318
|
i18n,
|
|
1304
1319
|
isExpected,
|
package/dist/server/index.mjs
CHANGED
|
@@ -1118,49 +1118,62 @@ async function shipeasy(opts) {
|
|
|
1118
1118
|
flags: bootstrap.flags,
|
|
1119
1119
|
configs: bootstrap.configs,
|
|
1120
1120
|
experiments: bootstrap.experiments,
|
|
1121
|
-
|
|
1122
|
-
return
|
|
1123
|
-
editLabels,
|
|
1121
|
+
getBootstrapData(emit) {
|
|
1122
|
+
return getBootstrapData(bootstrap, i18nData, {
|
|
1124
1123
|
i18nProfile: profile,
|
|
1125
|
-
anonId
|
|
1124
|
+
anonId,
|
|
1125
|
+
...emit
|
|
1126
|
+
});
|
|
1127
|
+
},
|
|
1128
|
+
getBootstrapTags(emit) {
|
|
1129
|
+
return getBootstrapTags(bootstrap, i18nData, {
|
|
1130
|
+
i18nProfile: profile,
|
|
1131
|
+
anonId,
|
|
1132
|
+
...emit
|
|
1126
1133
|
});
|
|
1127
1134
|
}
|
|
1128
1135
|
};
|
|
1129
1136
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1137
|
+
var DEFAULT_CDN = "https://cdn.shipeasy.ai";
|
|
1138
|
+
function getBootstrapData(bootstrap, i18nData, opts) {
|
|
1139
|
+
const base = (opts.baseUrl ?? DEFAULT_CDN).replace(/\/$/, "");
|
|
1133
1140
|
const profile = opts.i18nProfile ?? "en:prod";
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1141
|
+
const attrs = {
|
|
1142
|
+
"data-se-bootstrap": "",
|
|
1143
|
+
"data-flags": JSON.stringify(bootstrap?.flags ?? {}),
|
|
1144
|
+
"data-configs": JSON.stringify(bootstrap?.configs ?? {}),
|
|
1145
|
+
"data-experiments": JSON.stringify(bootstrap?.experiments ?? {}),
|
|
1146
|
+
"data-killswitches": JSON.stringify(bootstrap?.killswitches ?? {}),
|
|
1147
|
+
"data-i18n-profile": profile,
|
|
1148
|
+
"data-api-url": base
|
|
1142
1149
|
};
|
|
1143
|
-
if (
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
|
|
1156
|
-
parts.push(
|
|
1157
|
-
`(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
|
|
1158
|
-
);
|
|
1150
|
+
if (opts.anonId) attrs["data-anon-id"] = opts.anonId;
|
|
1151
|
+
const bootstrapTag = { src: `${base}/sdk/bootstrap.js`, attrs };
|
|
1152
|
+
let i18nLoader = null;
|
|
1153
|
+
const hasStrings = !!(i18nData?.strings && Object.keys(i18nData.strings).length > 0);
|
|
1154
|
+
if (hasStrings || opts.clientKey) {
|
|
1155
|
+
const i18nAttrs = { "data-profile": profile };
|
|
1156
|
+
if (opts.clientKey) i18nAttrs["data-key"] = opts.clientKey;
|
|
1157
|
+
if (hasStrings) {
|
|
1158
|
+
i18nAttrs["data-strings"] = JSON.stringify(i18nData.strings);
|
|
1159
|
+
i18nAttrs["data-locale"] = i18nData.locale;
|
|
1160
|
+
}
|
|
1161
|
+
i18nLoader = { src: `${base}/sdk/i18n/loader.js`, attrs: i18nAttrs };
|
|
1159
1162
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return
|
|
1163
|
+
return { bootstrap: bootstrapTag, i18nLoader };
|
|
1164
|
+
}
|
|
1165
|
+
function escapeAttr(v) {
|
|
1166
|
+
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1167
|
+
}
|
|
1168
|
+
function renderScriptTag(spec) {
|
|
1169
|
+
const attrs = Object.entries(spec.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`).join("");
|
|
1170
|
+
return `<script src="${escapeAttr(spec.src)}"${attrs}></script>`;
|
|
1171
|
+
}
|
|
1172
|
+
function getBootstrapTags(bootstrap, i18nData, opts) {
|
|
1173
|
+
const data = getBootstrapData(bootstrap, i18nData, opts);
|
|
1174
|
+
const tags = [data.bootstrap];
|
|
1175
|
+
if (data.i18nLoader) tags.push(data.i18nLoader);
|
|
1176
|
+
return tags.map(renderScriptTag).join("");
|
|
1164
1177
|
}
|
|
1165
1178
|
var flags = {
|
|
1166
1179
|
configure(opts) {
|
|
@@ -1254,7 +1267,8 @@ export {
|
|
|
1254
1267
|
createInMemoryStickyStore,
|
|
1255
1268
|
fetchLabelsForSSR,
|
|
1256
1269
|
flags,
|
|
1257
|
-
|
|
1270
|
+
getBootstrapData,
|
|
1271
|
+
getBootstrapTags,
|
|
1258
1272
|
getShipeasyServerClient,
|
|
1259
1273
|
i18n,
|
|
1260
1274
|
isExpected,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://shipeasy.ai",
|