@shelbyb/daywalker-sdk 0.1.4 → 0.2.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.
Files changed (43) hide show
  1. package/README.md +23 -0
  2. package/dist/analytics/index.d.ts +99 -0
  3. package/dist/analytics/index.js +352 -0
  4. package/dist/flags/client.d.ts +101 -0
  5. package/dist/flags/client.js +293 -0
  6. package/dist/flags/common/events.d.ts +59 -0
  7. package/dist/flags/common/events.js +178 -0
  8. package/dist/flags/common/index.d.ts +5 -0
  9. package/dist/flags/common/index.js +12 -0
  10. package/dist/flags/common/sse.d.ts +62 -0
  11. package/dist/flags/common/sse.js +183 -0
  12. package/dist/flags/engine/bucket.d.ts +21 -0
  13. package/dist/flags/engine/bucket.js +41 -0
  14. package/dist/flags/engine/context.d.ts +30 -0
  15. package/dist/flags/engine/context.js +80 -0
  16. package/dist/flags/engine/evaluate.d.ts +54 -0
  17. package/dist/flags/engine/evaluate.js +177 -0
  18. package/dist/flags/engine/index.d.ts +7 -0
  19. package/dist/flags/engine/index.js +7 -0
  20. package/dist/flags/engine/operators.d.ts +5 -0
  21. package/dist/flags/engine/operators.js +89 -0
  22. package/dist/flags/engine/semver.d.ts +13 -0
  23. package/dist/flags/engine/semver.js +35 -0
  24. package/dist/flags/engine/sha1.d.ts +6 -0
  25. package/dist/flags/engine/sha1.js +43 -0
  26. package/dist/flags/engine/types.d.ts +120 -0
  27. package/dist/flags/engine/types.js +17 -0
  28. package/dist/flags/node.d.ts +97 -0
  29. package/dist/flags/node.js +260 -0
  30. package/dist/flags/react.d.ts +27 -0
  31. package/dist/flags/react.js +62 -0
  32. package/dist/openfeature/server.d.ts +26 -0
  33. package/dist/openfeature/server.js +42 -0
  34. package/dist/openfeature/shared.d.ts +19 -0
  35. package/dist/openfeature/shared.js +40 -0
  36. package/dist/openfeature/web.d.ts +29 -0
  37. package/dist/openfeature/web.js +50 -0
  38. package/docs/analytics.md +32 -0
  39. package/docs/flags-node.md +49 -0
  40. package/docs/flags-react.md +35 -0
  41. package/docs/flags.md +53 -0
  42. package/docs/openfeature.md +30 -0
  43. package/package.json +61 -6
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ClientOptions, type ContextInput, type DevPulseClient, type EvaluationDetail, type FlagSet, type FlagValue } from "./client.js";
3
+ export type { ClientOptions, ContextInput, DevPulseClient, FlagSet, FlagValue };
4
+ export interface DevPulseProviderProps {
5
+ clientSideId: string;
6
+ context: ContextInput;
7
+ options: ClientOptions;
8
+ /** Use an existing client instead of creating one. */
9
+ client?: DevPulseClient;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * Provides a DevPulse client to the tree. On the server (SSR/RSC streaming)
14
+ * no network connection is opened; flags come from `options.bootstrap`, so the
15
+ * first client render matches the server HTML. Changing `context` calls identify().
16
+ */
17
+ export declare function DevPulseProvider({ clientSideId, context, options, client: external, children }: DevPulseProviderProps): import("react").JSX.Element;
18
+ export declare function useDevPulseClient(): DevPulseClient;
19
+ /** Current value of one flag; re-renders only when that flag changes. */
20
+ export declare function useFlag<T extends FlagValue>(key: string, defaultValue: T): T;
21
+ export declare function useFlagDetail<T extends FlagValue>(key: string, defaultValue: T): EvaluationDetail<T>;
22
+ /** All flag values; re-renders when any flag changes. */
23
+ export declare function useFlags(): Record<string, FlagValue>;
24
+ /** True once flags have loaded (always true when bootstrapped). */
25
+ export declare function useFlagsReady(): boolean;
26
+ /** Track a custom event for metrics and experiments. */
27
+ export declare function useTrack(): (key: string, data?: unknown, metricValue?: number) => void;
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createClient } from "./client.js";
4
+ const ClientContext = createContext(null);
5
+ const isBrowser = typeof window !== "undefined";
6
+ /**
7
+ * Provides a DevPulse client to the tree. On the server (SSR/RSC streaming)
8
+ * no network connection is opened; flags come from `options.bootstrap`, so the
9
+ * first client render matches the server HTML. Changing `context` calls identify().
10
+ */
11
+ export function DevPulseProvider({ clientSideId, context, options, client: external, children }) {
12
+ const [client] = useState(() => external ?? createClient(clientSideId, context, isBrowser ? options : { ...options, streaming: false, sendEvents: false, bootstrap: options.bootstrap ?? {} }));
13
+ const contextJson = JSON.stringify(context);
14
+ const first = useRef(true);
15
+ useEffect(() => {
16
+ if (first.current) {
17
+ first.current = false;
18
+ return;
19
+ }
20
+ void client.identify(JSON.parse(contextJson));
21
+ }, [client, contextJson]);
22
+ useEffect(() => () => void (!external && client.close()), [client, external]);
23
+ return _jsx(ClientContext.Provider, { value: client, children: children });
24
+ }
25
+ export function useDevPulseClient() {
26
+ const client = useContext(ClientContext);
27
+ if (!client)
28
+ throw new Error("useDevPulseClient must be used inside <DevPulseProvider>");
29
+ return client;
30
+ }
31
+ /** Current value of one flag; re-renders only when that flag changes. */
32
+ export function useFlag(key, defaultValue) {
33
+ return useFlagDetail(key, defaultValue).value;
34
+ }
35
+ export function useFlagDetail(key, defaultValue) {
36
+ const client = useDevPulseClient();
37
+ const defaultJson = JSON.stringify(defaultValue);
38
+ const subscribe = useMemo(() => (cb) => client.on(`change:${key}`, cb), [client, key]);
39
+ // Snapshot is the serialized value so useSyncExternalStore sees stable identities.
40
+ const snapshot = () => JSON.stringify(client.allFlags()[key] ?? null);
41
+ const serialized = useSyncExternalStore(subscribe, snapshot, snapshot);
42
+ // Record the evaluation (analytics) once per distinct value.
43
+ return useMemo(() => client.variationDetail(key, JSON.parse(defaultJson)), [client, key, defaultJson, serialized]); // eslint-disable-line react-hooks/exhaustive-deps
44
+ }
45
+ /** All flag values; re-renders when any flag changes. */
46
+ export function useFlags() {
47
+ const client = useDevPulseClient();
48
+ const subscribe = useMemo(() => (cb) => client.on("change", cb), [client]);
49
+ const json = useSyncExternalStore(subscribe, () => JSON.stringify(client.allFlags()), () => JSON.stringify(client.allFlags()));
50
+ return useMemo(() => JSON.parse(json), [json]);
51
+ }
52
+ /** True once flags have loaded (always true when bootstrapped). */
53
+ export function useFlagsReady() {
54
+ const client = useDevPulseClient();
55
+ const subscribe = useMemo(() => (cb) => client.on("ready", cb), [client]);
56
+ return useSyncExternalStore(subscribe, () => client.isInitialized, () => client.isInitialized);
57
+ }
58
+ /** Track a custom event for metrics and experiments. */
59
+ export function useTrack() {
60
+ const client = useDevPulseClient();
61
+ return (key, data, metricValue) => client.track(key, data, metricValue);
62
+ }
@@ -0,0 +1,26 @@
1
+ import type { EvaluationContext, JsonValue, Logger as OFLogger, Provider, ResolutionDetails } from "@openfeature/server-sdk";
2
+ import { OpenFeatureEventEmitter } from "@openfeature/server-sdk";
3
+ import { type DevPulseNodeClient, type NodeClientOptions } from "../flags/node.js";
4
+ /**
5
+ * OpenFeature provider for servers, backed by @shelbyb/daywalker-sdk/flags/node (local evaluation,
6
+ * real-time updates).
7
+ *
8
+ * await OpenFeature.setProviderAndWait(new DevPulseServerProvider(sdkKey, { baseUrl }));
9
+ * const enabled = await OpenFeature.getClient().getBooleanValue("new-checkout", false, { targetingKey: user.id });
10
+ */
11
+ export declare class DevPulseServerProvider implements Provider {
12
+ readonly metadata: {
13
+ readonly name: "devpulse";
14
+ };
15
+ readonly runsOn: "server";
16
+ readonly events: OpenFeatureEventEmitter;
17
+ readonly client: DevPulseNodeClient;
18
+ constructor(sdkKeyOrClient: string | DevPulseNodeClient, options?: NodeClientOptions);
19
+ initialize(): Promise<void>;
20
+ onClose(): Promise<void>;
21
+ private resolve;
22
+ resolveBooleanEvaluation(key: string, def: boolean, ctx: EvaluationContext, _logger: OFLogger): Promise<ResolutionDetails<boolean>>;
23
+ resolveStringEvaluation(key: string, def: string, ctx: EvaluationContext, _logger: OFLogger): Promise<ResolutionDetails<string>>;
24
+ resolveNumberEvaluation(key: string, def: number, ctx: EvaluationContext, _logger: OFLogger): Promise<ResolutionDetails<number>>;
25
+ resolveObjectEvaluation<T extends JsonValue>(key: string, def: T, ctx: EvaluationContext, _logger: OFLogger): Promise<ResolutionDetails<T>>;
26
+ }
@@ -0,0 +1,42 @@
1
+ import { OpenFeatureEventEmitter, ProviderEvents } from "@openfeature/server-sdk";
2
+ import { init } from "../flags/node.js";
3
+ import { toDevPulseContext, toResolution, typeCheck } from "./shared.js";
4
+ /**
5
+ * OpenFeature provider for servers, backed by @shelbyb/daywalker-sdk/flags/node (local evaluation,
6
+ * real-time updates).
7
+ *
8
+ * await OpenFeature.setProviderAndWait(new DevPulseServerProvider(sdkKey, { baseUrl }));
9
+ * const enabled = await OpenFeature.getClient().getBooleanValue("new-checkout", false, { targetingKey: user.id });
10
+ */
11
+ export class DevPulseServerProvider {
12
+ metadata = { name: "devpulse" };
13
+ runsOn = "server";
14
+ events = new OpenFeatureEventEmitter();
15
+ client;
16
+ constructor(sdkKeyOrClient, options) {
17
+ this.client = typeof sdkKeyOrClient === "string" ? init(sdkKeyOrClient, options) : sdkKeyOrClient;
18
+ this.client.on("update", ({ key }) => this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [key] }));
19
+ }
20
+ async initialize() {
21
+ await this.client.waitForInitialization();
22
+ }
23
+ async onClose() {
24
+ await this.client.close();
25
+ }
26
+ resolve(key, def, ctx, type) {
27
+ const detail = this.client.variationDetailSync(key, toDevPulseContext(ctx), def);
28
+ return toResolution(typeCheck(detail, type, def));
29
+ }
30
+ async resolveBooleanEvaluation(key, def, ctx, _logger) {
31
+ return this.resolve(key, def, ctx, "boolean");
32
+ }
33
+ async resolveStringEvaluation(key, def, ctx, _logger) {
34
+ return this.resolve(key, def, ctx, "string");
35
+ }
36
+ async resolveNumberEvaluation(key, def, ctx, _logger) {
37
+ return this.resolve(key, def, ctx, "number");
38
+ }
39
+ async resolveObjectEvaluation(key, def, ctx, _logger) {
40
+ return this.resolve(key, def, ctx, "object");
41
+ }
42
+ }
@@ -0,0 +1,19 @@
1
+ import type { EvaluationContext, ResolutionDetails } from "@openfeature/core";
2
+ type Reason = {
3
+ kind: string;
4
+ errorKind?: string;
5
+ inExperiment?: boolean;
6
+ } | null | undefined;
7
+ type Detail<T> = {
8
+ value: T;
9
+ variationIndex: number | null;
10
+ reason: Reason;
11
+ };
12
+ /**
13
+ * OpenFeature context → DevPulse context. `targetingKey` becomes the key;
14
+ * `kind` selects the context kind (default "user"); multi-kind contexts pass through.
15
+ */
16
+ export declare function toDevPulseContext(ctx: EvaluationContext): Record<string, unknown>;
17
+ export declare function toResolution<T>(detail: Detail<T>): ResolutionDetails<T>;
18
+ export declare function typeCheck<T>(detail: Detail<unknown>, expected: string, defaultValue: T): Detail<T>;
19
+ export {};
@@ -0,0 +1,40 @@
1
+ import { ErrorCode, StandardResolutionReasons } from "@openfeature/core";
2
+ /**
3
+ * OpenFeature context → DevPulse context. `targetingKey` becomes the key;
4
+ * `kind` selects the context kind (default "user"); multi-kind contexts pass through.
5
+ */
6
+ export function toDevPulseContext(ctx) {
7
+ const { targetingKey, ...rest } = ctx;
8
+ if (rest.kind === "multi")
9
+ return rest;
10
+ return { ...rest, kind: typeof rest.kind === "string" ? rest.kind : "user", key: targetingKey ?? rest.key };
11
+ }
12
+ const ERRORS = {
13
+ FLAG_NOT_FOUND: ErrorCode.FLAG_NOT_FOUND,
14
+ WRONG_TYPE: ErrorCode.TYPE_MISMATCH,
15
+ USER_NOT_SPECIFIED: ErrorCode.TARGETING_KEY_MISSING,
16
+ CLIENT_NOT_READY: ErrorCode.PROVIDER_NOT_READY,
17
+ MALFORMED_FLAG: ErrorCode.PARSE_ERROR,
18
+ };
19
+ export function toResolution(detail) {
20
+ const r = detail.reason;
21
+ if (r?.kind === "ERROR") {
22
+ return { value: detail.value, reason: StandardResolutionReasons.ERROR, errorCode: ERRORS[r.errorKind ?? ""] ?? ErrorCode.GENERAL, errorMessage: r.errorKind };
23
+ }
24
+ let reason = StandardResolutionReasons.UNKNOWN;
25
+ if (r?.kind === "OFF")
26
+ reason = StandardResolutionReasons.DISABLED;
27
+ else if (r?.kind === "TARGET_MATCH" || r?.kind === "RULE_MATCH")
28
+ reason = r.inExperiment ? StandardResolutionReasons.SPLIT : StandardResolutionReasons.TARGETING_MATCH;
29
+ else if (r?.kind === "FALLTHROUGH")
30
+ reason = r.inExperiment ? StandardResolutionReasons.SPLIT : StandardResolutionReasons.DEFAULT;
31
+ else if (r?.kind === "PREREQUISITE_FAILED")
32
+ reason = StandardResolutionReasons.DISABLED;
33
+ return { value: detail.value, reason, ...(detail.variationIndex !== null ? { variant: String(detail.variationIndex) } : {}), flagMetadata: r ? { devpulseReason: r.kind } : {} };
34
+ }
35
+ export function typeCheck(detail, expected, defaultValue) {
36
+ const actual = Array.isArray(detail.value) ? "object" : typeof detail.value;
37
+ if (detail.reason?.kind !== "ERROR" && actual !== expected)
38
+ return { value: defaultValue, variationIndex: null, reason: { kind: "ERROR", errorKind: "WRONG_TYPE" } };
39
+ return detail;
40
+ }
@@ -0,0 +1,29 @@
1
+ import type { EvaluationContext, JsonValue, Logger as OFLogger, Provider, ResolutionDetails } from "@openfeature/web-sdk";
2
+ import { OpenFeatureEventEmitter } from "@openfeature/web-sdk";
3
+ import { type ClientOptions } from "../flags/client.js";
4
+ /**
5
+ * OpenFeature provider for browsers, backed by @shelbyb/daywalker-sdk/flags. Flag
6
+ * values update in real time and emit PROVIDER_CONFIGURATION_CHANGED.
7
+ *
8
+ * await OpenFeature.setContext({ targetingKey: user.id });
9
+ * await OpenFeature.setProviderAndWait(new DevPulseWebProvider(clientSideId, { baseUrl }));
10
+ */
11
+ export declare class DevPulseWebProvider implements Provider {
12
+ private readonly clientSideId;
13
+ private readonly options;
14
+ readonly metadata: {
15
+ readonly name: "devpulse";
16
+ };
17
+ readonly runsOn: "client";
18
+ readonly events: OpenFeatureEventEmitter;
19
+ private client;
20
+ constructor(clientSideId: string, options: ClientOptions);
21
+ initialize(context?: EvaluationContext): Promise<void>;
22
+ onContextChange(_old: EvaluationContext, next: EvaluationContext): Promise<void>;
23
+ onClose(): Promise<void>;
24
+ private resolve;
25
+ resolveBooleanEvaluation(key: string, def: boolean, _ctx: EvaluationContext, _logger: OFLogger): ResolutionDetails<boolean>;
26
+ resolveStringEvaluation(key: string, def: string, _ctx: EvaluationContext, _logger: OFLogger): ResolutionDetails<string>;
27
+ resolveNumberEvaluation(key: string, def: number, _ctx: EvaluationContext, _logger: OFLogger): ResolutionDetails<number>;
28
+ resolveObjectEvaluation<T extends JsonValue>(key: string, def: T, _ctx: EvaluationContext, _logger: OFLogger): ResolutionDetails<T>;
29
+ }
@@ -0,0 +1,50 @@
1
+ import { OpenFeatureEventEmitter, ProviderEvents } from "@openfeature/web-sdk";
2
+ import { createClient } from "../flags/client.js";
3
+ import { toDevPulseContext, toResolution, typeCheck } from "./shared.js";
4
+ /**
5
+ * OpenFeature provider for browsers, backed by @shelbyb/daywalker-sdk/flags. Flag
6
+ * values update in real time and emit PROVIDER_CONFIGURATION_CHANGED.
7
+ *
8
+ * await OpenFeature.setContext({ targetingKey: user.id });
9
+ * await OpenFeature.setProviderAndWait(new DevPulseWebProvider(clientSideId, { baseUrl }));
10
+ */
11
+ export class DevPulseWebProvider {
12
+ clientSideId;
13
+ options;
14
+ metadata = { name: "devpulse" };
15
+ runsOn = "client";
16
+ events = new OpenFeatureEventEmitter();
17
+ client = null;
18
+ constructor(clientSideId, options) {
19
+ this.clientSideId = clientSideId;
20
+ this.options = options;
21
+ }
22
+ async initialize(context) {
23
+ this.client = createClient(this.clientSideId, toDevPulseContext(context ?? {}), { ...this.options, withReasons: true });
24
+ this.client.on("change", (changes) => this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: Object.keys(changes) }));
25
+ await this.client.waitForInitialization();
26
+ }
27
+ async onContextChange(_old, next) {
28
+ await this.client?.identify(toDevPulseContext(next));
29
+ }
30
+ async onClose() {
31
+ await this.client?.close();
32
+ }
33
+ resolve(key, def, type) {
34
+ if (!this.client)
35
+ return toResolution({ value: def, variationIndex: null, reason: { kind: "ERROR", errorKind: "CLIENT_NOT_READY" } });
36
+ return toResolution(typeCheck(this.client.variationDetail(key, def), type, def));
37
+ }
38
+ resolveBooleanEvaluation(key, def, _ctx, _logger) {
39
+ return this.resolve(key, def, "boolean");
40
+ }
41
+ resolveStringEvaluation(key, def, _ctx, _logger) {
42
+ return this.resolve(key, def, "string");
43
+ }
44
+ resolveNumberEvaluation(key, def, _ctx, _logger) {
45
+ return this.resolve(key, def, "number");
46
+ }
47
+ resolveObjectEvaluation(key, def, _ctx, _logger) {
48
+ return this.resolve(key, def, "object");
49
+ }
50
+ }
@@ -0,0 +1,32 @@
1
+ # @shelbyb/daywalker-sdk/analytics
2
+
3
+ Product analytics for browsers and Node: events, automatic page views (including client-side navigation), sessions, identify, super properties and feature-flag properties.
4
+
5
+ ```sh
6
+ npm install @shelbyb/daywalker-sdk
7
+ ```
8
+
9
+ ```ts
10
+ import { createAnalytics } from "@shelbyb/daywalker-sdk/analytics";
11
+
12
+ const analytics = createAnalytics({ writeKey: "dpa_…", host: "https://devpulse.example.com" });
13
+
14
+ analytics.capture("project_created", { template: "nextjs" });
15
+ analytics.identify(user.id, { email: user.email, plan: user.plan }); // merges the anonymous history
16
+ analytics.register({ app_version: "2.4.0" }); // sent with every event
17
+ analytics.reset(); // on logout
18
+ ```
19
+
20
+ Server-side (Node), pass who did it and flush before exit:
21
+
22
+ ```ts
23
+ const analytics = createAnalytics({ writeKey: process.env.DEVPULSE_WRITE_KEY!, host: "https://devpulse.example.com" });
24
+ analytics.capture("invoice_paid", { amount: 49 }, { distinctId: customer.id });
25
+ await analytics.shutdown();
26
+ ```
27
+
28
+ Add each flag's value to events (for breakdowns by variation) with `flags: client` where `client` is a `@shelbyb/daywalker-sdk/flags` instance (anything with `allFlags()`).
29
+
30
+ Options: `autocapturePageviews` (default true), `flushAt` (20), `flushInterval` (2s browser / 10s Node), `maxQueueSize` (1000), `persistence` (`localStorage` | `memory`), `sessionTimeoutMinutes` (30), `respectDoNotTrack`, `optedOut` (consent), `onError`.
31
+
32
+ Events are sent to `POST {host}/a/batch`, which also accepts PostHog's capture format.
@@ -0,0 +1,49 @@
1
+ # @shelbyb/daywalker-sdk/flags/node
2
+
3
+ Server-side feature flags for Node.js from [DevPulse](https://github.com/shelbyb/devpulse). The SDK downloads your flag rules once, keeps them current over a streaming connection, and evaluates locally, so `variation()` adds no network latency.
4
+
5
+ ```bash
6
+ npm install @shelbyb/daywalker-sdk
7
+ ```
8
+
9
+ ```ts
10
+ import { init } from "@shelbyb/daywalker-sdk/flags/node";
11
+
12
+ export const flags = init(process.env.DEVPULSE_SDK_KEY!, { baseUrl: "https://devpulse.example.com" });
13
+ await flags.waitForInitialization({ timeoutMs: 5000 });
14
+
15
+ const context = { kind: "user", key: user.id, email: user.email, plan: user.plan };
16
+ if (await flags.variation("new-checkout", context, false)) {
17
+ // …
18
+ }
19
+
20
+ flags.on("update:new-checkout", () => console.log("new-checkout changed"));
21
+ flags.track("checkout-completed", context, undefined, order.total);
22
+ ```
23
+
24
+ Create one client per process and reuse it. Call `await flags.close()` on shutdown to flush analytics.
25
+
26
+ ## Next.js (App Router): bootstrap the browser without a flicker
27
+
28
+ ```tsx
29
+ // app/layout.tsx (Server Component)
30
+ import { flags } from "@/lib/flags";
31
+ import { Providers } from "./providers";
32
+
33
+ export default async function Layout({ children }) {
34
+ const context = await currentUserContext();
35
+ const bootstrap = (await flags.allFlagsState(context, { clientSideOnly: true })).toJSON();
36
+ const hash = flags.secureModeHash(context); // if secure mode is on
37
+ return <Providers context={context} bootstrap={bootstrap} hash={hash}>{children}</Providers>;
38
+ }
39
+ ```
40
+
41
+ See `@shelbyb/daywalker-sdk/flags/react` for the `Providers` component.
42
+
43
+ ## Options
44
+
45
+ `baseUrl`, `stream` (default `true`; falls back to polling if streaming keeps failing), `pollIntervalMs` (≥ 30s), `sendEvents`, `privateAttributes`, `allAttributesPrivate`, `offline`, `initialData` (fixtures for tests), `logger`.
46
+
47
+ ## API
48
+
49
+ `variation` / `variationDetail` (async) and `variationSync` / `variationDetailSync`, typed helpers (`boolVariation`, …), `allFlagsState(context, { clientSideOnly, withReasons })`, `secureModeHash(context)`, `track`, `identify`, `on("ready" | "failed" | "update" | "update:<key>")`, `flush()`, `close()`.
@@ -0,0 +1,35 @@
1
+ # @shelbyb/daywalker-sdk/flags/react
2
+
3
+ React provider and hooks for [DevPulse](https://github.com/shelbyb/devpulse) feature flags. Components re-render when the flags they read change, in real time.
4
+
5
+ ```bash
6
+ npm install @shelbyb/daywalker-sdk
7
+ ```
8
+
9
+ ```tsx
10
+ "use client";
11
+ import { DevPulseProvider, useFlag, useTrack } from "@shelbyb/daywalker-sdk/flags/react";
12
+
13
+ export function Providers({ context, bootstrap, hash, children }) {
14
+ return (
15
+ <DevPulseProvider
16
+ clientSideId={process.env.NEXT_PUBLIC_DEVPULSE_CLIENT_ID!}
17
+ context={context}
18
+ options={{ baseUrl: process.env.NEXT_PUBLIC_DEVPULSE_URL!, bootstrap, hash }}
19
+ >
20
+ {children}
21
+ </DevPulseProvider>
22
+ );
23
+ }
24
+
25
+ function Checkout() {
26
+ const newCheckout = useFlag("new-checkout", false);
27
+ const track = useTrack();
28
+ return newCheckout ? <NewCheckout onDone={(total) => track("checkout-completed", undefined, total)} /> : <OldCheckout />;
29
+ }
30
+ ```
31
+
32
+ - **SSR / React Server Components:** pass `bootstrap` from `@shelbyb/daywalker-sdk/flags/node`'s `allFlagsState(context, { clientSideOnly: true }).toJSON()`. The server render and first client render then match, with no loading flicker. No network connection is opened during server rendering.
33
+ - Changing the `context` prop calls `identify()`.
34
+
35
+ Hooks: `useFlag(key, default)`, `useFlagDetail(key, default)`, `useFlags()`, `useFlagsReady()`, `useTrack()`, `useDevPulseClient()`.
package/docs/flags.md ADDED
@@ -0,0 +1,53 @@
1
+ # @shelbyb/daywalker-sdk/flags
2
+
3
+ Feature flags for browsers (and any JavaScript client) from [DevPulse](https://github.com/shelbyb/devpulse). Flag changes arrive in real time over Server-Sent Events, typically within tens of milliseconds of a change in the dashboard.
4
+
5
+ ```bash
6
+ npm install @shelbyb/daywalker-sdk
7
+ ```
8
+
9
+ ```ts
10
+ import { createClient } from "@shelbyb/daywalker-sdk/flags";
11
+
12
+ const client = createClient(
13
+ "YOUR_CLIENT_SIDE_ID", // Project → Environments & keys
14
+ { kind: "user", key: user.id, email: user.email },
15
+ { baseUrl: "https://devpulse.example.com" },
16
+ );
17
+
18
+ await client.waitForInitialization({ timeoutMs: 3000 });
19
+
20
+ if (client.variation("new-checkout", false)) showNewCheckout();
21
+
22
+ // React to changes made in the dashboard, live
23
+ client.on("change:new-checkout", (enabled) => (enabled ? showNewCheckout() : showOldCheckout()));
24
+
25
+ // Metrics and experiments
26
+ client.track("checkout-completed", { items: 3 }, 129.5);
27
+
28
+ // After login
29
+ await client.identify({ kind: "user", key: newUser.id });
30
+ ```
31
+
32
+ Only flags marked **Available to client-side SDKs** are visible.
33
+
34
+ ## Options
35
+
36
+ | Option | Default | |
37
+ | --- | --- | --- |
38
+ | `baseUrl` | — | Your DevPulse URL |
39
+ | `streaming` | `true` | Real-time updates (SSE). `false` fetches once |
40
+ | `bootstrap` | — | Initial values, e.g. from `@shelbyb/daywalker-sdk/flags/node`'s `allFlagsState().toJSON()` |
41
+ | `hash` | — | Secure mode hash (`HMAC-SHA256(sdkKey, contextKey)`), computed on your server |
42
+ | `withReasons` | `false` | Include evaluation reasons for `variationDetail()` |
43
+ | `sendEvents` | `true` | Analytics for insights and experiments |
44
+ | `privateAttributes` / `allAttributesPrivate` | — | Attributes never sent to DevPulse |
45
+ | `cache` | `false` | Persist the last flags in `localStorage` and use them while loading |
46
+
47
+ ## Contexts
48
+
49
+ Single kind: `{ kind: "user", key: "u1", plan: "pro" }`. Multi-kind: `{ kind: "multi", user: { key: "u1" }, organization: { key: "acme" } }`.
50
+
51
+ ## API
52
+
53
+ `variation(key, default)`, `variationDetail(key, default)`, `boolVariation` / `stringVariation` / `numberVariation` / `jsonVariation`, `allFlags()`, `identify(context)`, `track(key, data?, metricValue?)`, `on("ready" | "failed" | "change" | "change:<key>", fn)`, `flush()`, `close()`.
@@ -0,0 +1,30 @@
1
+ # @shelbyb/daywalker-sdk/openfeature
2
+
3
+ [OpenFeature](https://openfeature.dev) providers for DevPulse. Use the vendor-neutral OpenFeature API and swap providers without touching application code.
4
+
5
+ ```bash
6
+ npm install @shelbyb/daywalker-sdk @openfeature/server-sdk # servers
7
+ npm install @shelbyb/daywalker-sdk @openfeature/web-sdk # browsers
8
+ ```
9
+
10
+ ```ts
11
+ // Server
12
+ import { OpenFeature } from "@openfeature/server-sdk";
13
+ import { DevPulseServerProvider } from "@shelbyb/daywalker-sdk/openfeature/server";
14
+
15
+ await OpenFeature.setProviderAndWait(new DevPulseServerProvider(process.env.DEVPULSE_SDK_KEY!, { baseUrl }));
16
+ const client = OpenFeature.getClient();
17
+ const enabled = await client.getBooleanValue("new-checkout", false, { targetingKey: user.id, plan: "pro" });
18
+ ```
19
+
20
+ ```ts
21
+ // Browser
22
+ import { OpenFeature } from "@openfeature/web-sdk";
23
+ import { DevPulseWebProvider } from "@shelbyb/daywalker-sdk/openfeature/web";
24
+
25
+ await OpenFeature.setContext({ targetingKey: user.id });
26
+ await OpenFeature.setProviderAndWait(new DevPulseWebProvider(clientSideId, { baseUrl }));
27
+ OpenFeature.getClient().getBooleanValue("new-checkout", false);
28
+ ```
29
+
30
+ `targetingKey` maps to the context key, and a `kind` attribute selects the context kind (default `user`). Flag changes emit `PROVIDER_CONFIGURATION_CHANGED`. Reasons map to OpenFeature's standard reasons: `TARGETING_MATCH`, `SPLIT`, `DEFAULT`, `DISABLED`, `ERROR`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shelbyb/daywalker-sdk",
3
- "version": "0.1.4",
4
- "description": "Validate Daywalker API tokens and make domain-signed calls to Daywalker Auth from any service.",
3
+ "version": "0.2.0",
4
+ "description": "Daywalker Auth service validation plus the DevPulse client SDKs (feature flags, OpenFeature, product analytics) in one zero-dependency package.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -21,6 +21,34 @@
21
21
  "./express": {
22
22
  "types": "./dist/express.d.ts",
23
23
  "import": "./dist/express.js"
24
+ },
25
+ "./flags": {
26
+ "types": "./dist/flags/client.d.ts",
27
+ "import": "./dist/flags/client.js"
28
+ },
29
+ "./flags/node": {
30
+ "types": "./dist/flags/node.d.ts",
31
+ "import": "./dist/flags/node.js"
32
+ },
33
+ "./flags/react": {
34
+ "types": "./dist/flags/react.d.ts",
35
+ "import": "./dist/flags/react.js"
36
+ },
37
+ "./flags/engine": {
38
+ "types": "./dist/flags/engine/index.d.ts",
39
+ "import": "./dist/flags/engine/index.js"
40
+ },
41
+ "./openfeature/server": {
42
+ "types": "./dist/openfeature/server.d.ts",
43
+ "import": "./dist/openfeature/server.js"
44
+ },
45
+ "./openfeature/web": {
46
+ "types": "./dist/openfeature/web.d.ts",
47
+ "import": "./dist/openfeature/web.js"
48
+ },
49
+ "./analytics": {
50
+ "types": "./dist/analytics/index.d.ts",
51
+ "import": "./dist/analytics/index.js"
24
52
  }
25
53
  },
26
54
  "bin": {
@@ -29,6 +57,7 @@
29
57
  "files": [
30
58
  "dist",
31
59
  "assets",
60
+ "docs",
32
61
  "README.md",
33
62
  "LICENSE"
34
63
  ],
@@ -52,19 +81,45 @@
52
81
  },
53
82
  "license": "MIT",
54
83
  "devDependencies": {
84
+ "@openfeature/core": "^1.12.0",
85
+ "@openfeature/server-sdk": "^1.23.0",
86
+ "@openfeature/web-sdk": "^1.10.0",
55
87
  "@types/node": "^22.13.5",
56
- "typescript": "^5.9.3",
57
- "tsx": "^4.23.15"
88
+ "@types/react": "^19.3.0",
89
+ "react": "^19.3.0",
90
+ "tsx": "^4.23.15",
91
+ "typescript": "^5.9.3"
58
92
  },
59
93
  "keywords": [
60
- "daywalker",
61
94
  "api-token",
62
95
  "authentication",
96
+ "daywalker",
97
+ "devpulse",
63
98
  "ed25519",
99
+ "feature-flags",
100
+ "openfeature",
101
+ "product-analytics",
64
102
  "request-signing"
65
103
  ],
66
104
  "homepage": "https://auth.daywalker.dev/docs",
67
105
  "bugs": {
68
106
  "url": "https://github.com/Shelbyb/daywalker-sdk/issues"
69
- }
107
+ },
108
+ "peerDependencies": {
109
+ "@openfeature/server-sdk": "^1.17.0",
110
+ "@openfeature/web-sdk": "^1.5.0",
111
+ "react": ">=18"
112
+ },
113
+ "peerDependenciesMeta": {
114
+ "react": {
115
+ "optional": true
116
+ },
117
+ "@openfeature/server-sdk": {
118
+ "optional": true
119
+ },
120
+ "@openfeature/web-sdk": {
121
+ "optional": true
122
+ }
123
+ },
124
+ "sideEffects": false
70
125
  }