@volter/twin-segment 0.1.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 +154 -0
- package/client/segment-mirror.css +45 -0
- package/client/segment-mirror.tsx +154 -0
- package/dist/client/segment-mirror.bundle.js +342 -0
- package/dist/client/segment-mirror.css +45 -0
- package/dist/client/segment-mirror.d.ts +34 -0
- package/dist/client/segment-mirror.js +80 -0
- package/dist/client/segment-mirror.tsx +154 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +34 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +53 -0
- package/dist/src/segment-budget.d.ts +41 -0
- package/dist/src/segment-budget.js +112 -0
- package/dist/src/segment-capabilities.d.ts +12 -0
- package/dist/src/segment-capabilities.gen.d.ts +3 -0
- package/dist/src/segment-capabilities.gen.js +22 -0
- package/dist/src/segment-capabilities.js +907 -0
- package/dist/src/segment-conformance.d.ts +8 -0
- package/dist/src/segment-conformance.js +106 -0
- package/dist/src/segment-connector.d.ts +76 -0
- package/dist/src/segment-connector.js +226 -0
- package/dist/src/segment-mirror-ui.d.ts +42 -0
- package/dist/src/segment-mirror-ui.js +143 -0
- package/dist/src/segment-server.d.ts +25 -0
- package/dist/src/segment-server.js +90 -0
- package/dist/src/segment-surface.gen.d.ts +48 -0
- package/dist/src/segment-surface.gen.js +267 -0
- package/dist/src/segment-twin.d.ts +98 -0
- package/dist/src/segment-twin.js +543 -0
- package/package.json +59 -0
- package/src/cli.ts +30 -0
- package/src/index.ts +87 -0
- package/src/segment-budget.ts +138 -0
- package/src/segment-capabilities.gen.ts +25 -0
- package/src/segment-capabilities.ts +988 -0
- package/src/segment-conformance.ts +123 -0
- package/src/segment-connector.ts +233 -0
- package/src/segment-journey.uitest.ts +116 -0
- package/src/segment-mirror-ui.ts +157 -0
- package/src/segment-server.ts +95 -0
- package/src/segment-surface.gen.ts +277 -0
- package/src/segment-twin.ts +664 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0f1115;
|
|
3
|
+
--panel: #171a21;
|
|
4
|
+
--line: #262b35;
|
|
5
|
+
--text: #e6e9ef;
|
|
6
|
+
--muted: #9aa4b2;
|
|
7
|
+
--accent: #292929;
|
|
8
|
+
--track: #52bd94;
|
|
9
|
+
--identity: #6aa9f0;
|
|
10
|
+
--screenish: #b98cf0;
|
|
11
|
+
--dropped: #d24b4b;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body { margin: 0; font-family: -apple-system, system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
15
|
+
.app { max-width: 1040px; margin: 0 auto; padding: 0 20px 32px; }
|
|
16
|
+
.app-header { display: flex; align-items: center; justify-content: space-between; height: 60px; border-bottom: 1px solid var(--line); }
|
|
17
|
+
.app-title { font-size: 18px; font-weight: 700; margin: 0; letter-spacing: 0.02em; }
|
|
18
|
+
.counts { display: flex; gap: 14px; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
19
|
+
.count-dropped { color: var(--dropped); }
|
|
20
|
+
.app-nav { display: flex; gap: 6px; padding: 14px 0; }
|
|
21
|
+
.nav-item { background: none; border: 1px solid transparent; color: var(--muted); padding: 7px 14px; border-radius: 999px; cursor: pointer; font-size: 14px; }
|
|
22
|
+
.nav-item:hover { color: var(--text); }
|
|
23
|
+
.nav-item-active { background: var(--accent); border-color: var(--line); color: var(--text); }
|
|
24
|
+
.event-stream, .dropped-stream { list-style: none; margin: 0; padding: 0; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
25
|
+
.event-row, .dropped-row { padding: 12px 14px; border-bottom: 1px solid var(--line); }
|
|
26
|
+
.event-row:last-child, .dropped-row:last-child { border-bottom: none; }
|
|
27
|
+
.event-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
28
|
+
.event-clock { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
29
|
+
.event-label { font-weight: 600; font-size: 14px; }
|
|
30
|
+
.event-subject, .drop-endpoint { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
31
|
+
.drop-reason { color: var(--dropped); font-size: 13px; }
|
|
32
|
+
.drop-reason-twin { font-size: 11px; color: var(--muted); border: 1px dashed var(--line); border-radius: 4px; padding: 1px 6px; }
|
|
33
|
+
.stream-note { color: var(--muted); font-size: 12px; margin: 0 0 10px; }
|
|
34
|
+
.stream-note code { color: var(--text); }
|
|
35
|
+
.pill { padding: 3px 10px; border-radius: 999px; font-size: 12px; white-space: nowrap; }
|
|
36
|
+
.pill-track { background: rgba(82,189,148,0.18); color: var(--track); }
|
|
37
|
+
.pill-identity { background: rgba(106,169,240,0.18); color: var(--identity); }
|
|
38
|
+
.pill-screenish { background: rgba(185,140,240,0.18); color: var(--screenish); }
|
|
39
|
+
.pill-dropped { background: rgba(210,75,75,0.18); color: var(--dropped); }
|
|
40
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
41
|
+
.chips-empty { color: var(--muted); font-size: 12px; }
|
|
42
|
+
.chip { display: inline-flex; gap: 6px; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px; font-size: 12px; }
|
|
43
|
+
.chip-key { color: var(--muted); }
|
|
44
|
+
.chip-value { font-variant-numeric: tabular-nums; }
|
|
45
|
+
.empty { color: var(--muted); padding: 24px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type SegmentRow } from '../src/segment-mirror-ui.js';
|
|
2
|
+
/** Read one named projection off the twin's store door. */
|
|
3
|
+
export declare function store(name: string): Promise<SegmentRow[]>;
|
|
4
|
+
/** The property/trait chips on an expanded row — what a debugger reader is actually looking for
|
|
5
|
+
* when an event "arrived but is wrong". */
|
|
6
|
+
export declare function PropertyChips({ row }: {
|
|
7
|
+
row: SegmentRow;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
/** One row of the accepted stream. */
|
|
10
|
+
export declare function EventRow({ row }: {
|
|
11
|
+
row: SegmentRow;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* One row of the dropped stream.
|
|
15
|
+
*
|
|
16
|
+
* THIS PANE IS THE TWIN'S, NOT THE VENDOR'S, and the screen says so. Segment's real Source
|
|
17
|
+
* Debugger shows what a source RECEIVED; it has no "accepted-then-silently-dropped, with a reason"
|
|
18
|
+
* view, because the vendor answers 200 and discards. This twin records the refusal instead, which
|
|
19
|
+
* is the only way "returned 200 and never arrived" becomes observable — a superset of the vendor's
|
|
20
|
+
* screen, and the honest label for it. Only `no_user_anon_id` is a reason Segment itself prints
|
|
21
|
+
* (its Errors page); the other six are this pack's own descriptive strings, which is why the row
|
|
22
|
+
* marks them.
|
|
23
|
+
*/
|
|
24
|
+
export declare function DroppedRow({ row }: {
|
|
25
|
+
row: SegmentRow;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/** The accepted stream, newest last (the order the source received them). */
|
|
28
|
+
export declare function EventStream({ rows }: {
|
|
29
|
+
rows: SegmentRow[];
|
|
30
|
+
}): import("react").JSX.Element;
|
|
31
|
+
/** The dropped stream. */
|
|
32
|
+
export declare function DroppedStream({ rows }: {
|
|
33
|
+
rows: SegmentRow[];
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// Segment mirror — the SOURCE DEBUGGER, as a React/TSX client. Every byte on screen is fetched
|
|
3
|
+
// from the twin's OWN store door on the same origin (`/twin/store/events`, `/twin/store/dropped`)
|
|
4
|
+
// — the kernel's read-only named-projection door, and the sanctioned way a mirror reads twin
|
|
5
|
+
// state. API↔UI parity: this is the projection the ingest routes fold into, not a second copy.
|
|
6
|
+
import { createRoot } from 'react-dom/client';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { PILL_CLASS, clockLabel, messageLabel, propertyChips, subjectOf, typeTone, } from "../src/segment-mirror-ui.js";
|
|
9
|
+
// WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative, as
|
|
10
|
+
// before); served under a path prefix with a <base> tag every read and write resolves inside that
|
|
11
|
+
// prefix instead of escaping it.
|
|
12
|
+
const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
|
|
13
|
+
const STREAMS = [
|
|
14
|
+
{ key: 'events', label: 'Accepted' },
|
|
15
|
+
{ key: 'dropped', label: 'Dropped' },
|
|
16
|
+
];
|
|
17
|
+
/** Read one named projection off the twin's store door. */
|
|
18
|
+
export async function store(name) {
|
|
19
|
+
const res = await fetch(`${WIRE_BASE}/twin/store/${name}`);
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
return [];
|
|
22
|
+
const body = (await res.json());
|
|
23
|
+
return Array.isArray(body) ? body : [];
|
|
24
|
+
}
|
|
25
|
+
/** The property/trait chips on an expanded row — what a debugger reader is actually looking for
|
|
26
|
+
* when an event "arrived but is wrong". */
|
|
27
|
+
export function PropertyChips({ row }) {
|
|
28
|
+
const chips = [...propertyChips(row.properties), ...propertyChips(row.traits)];
|
|
29
|
+
if (chips.length === 0)
|
|
30
|
+
return _jsx("div", { className: "chips chips-empty", children: "no properties" });
|
|
31
|
+
return (_jsx("div", { className: "chips", children: chips.map((c) => (_jsxs("span", { className: "chip", children: [_jsx("span", { className: "chip-key", children: c.key }), _jsx("span", { className: "chip-value", children: c.value })] }, c.key))) }));
|
|
32
|
+
}
|
|
33
|
+
/** One row of the accepted stream. */
|
|
34
|
+
export function EventRow({ row }) {
|
|
35
|
+
const tone = typeTone(row.messageType);
|
|
36
|
+
return (_jsxs("li", { className: "event-row", "data-message-id": String(row.messageId ?? ''), children: [_jsxs("div", { className: "event-head", children: [_jsx("span", { className: "event-clock", children: clockLabel(row.receivedAt) }), _jsx("span", { className: PILL_CLASS[tone], children: String(row.messageType ?? '') }), _jsx("span", { className: "event-label", children: messageLabel(row) }), _jsx("span", { className: "event-subject", children: subjectOf(row) })] }), _jsx(PropertyChips, { row: row })] }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One row of the dropped stream.
|
|
40
|
+
*
|
|
41
|
+
* THIS PANE IS THE TWIN'S, NOT THE VENDOR'S, and the screen says so. Segment's real Source
|
|
42
|
+
* Debugger shows what a source RECEIVED; it has no "accepted-then-silently-dropped, with a reason"
|
|
43
|
+
* view, because the vendor answers 200 and discards. This twin records the refusal instead, which
|
|
44
|
+
* is the only way "returned 200 and never arrived" becomes observable — a superset of the vendor's
|
|
45
|
+
* screen, and the honest label for it. Only `no_user_anon_id` is a reason Segment itself prints
|
|
46
|
+
* (its Errors page); the other six are this pack's own descriptive strings, which is why the row
|
|
47
|
+
* marks them.
|
|
48
|
+
*/
|
|
49
|
+
export function DroppedRow({ row }) {
|
|
50
|
+
return (_jsx("li", { className: "dropped-row", "data-message-id": String(row.messageId ?? ''), children: _jsxs("div", { className: "event-head", children: [_jsx("span", { className: PILL_CLASS.dropped, children: "dropped" }), _jsx("span", { className: "event-label", children: String(row.messageType ?? '') }), _jsx("span", { className: "drop-reason", children: String(row.reason ?? 'unknown') }), String(row.reason ?? '') === 'no_user_anon_id'
|
|
51
|
+
? null
|
|
52
|
+
: _jsx("span", { className: "drop-reason-twin", title: "This reason is a twin-authored label. Segment prints only `no_user_anon_id`.", children: "twin label" }), _jsx("span", { className: "drop-endpoint", children: String(row.endpoint ?? '') })] }) }));
|
|
53
|
+
}
|
|
54
|
+
/** The accepted stream, newest last (the order the source received them). */
|
|
55
|
+
export function EventStream({ rows }) {
|
|
56
|
+
if (rows.length === 0)
|
|
57
|
+
return _jsx("div", { className: "empty", children: "No events yet. Send one and it appears here." });
|
|
58
|
+
return _jsx("ul", { className: "event-stream", children: rows.map((r) => _jsx(EventRow, { row: r }, String(r.messageId))) });
|
|
59
|
+
}
|
|
60
|
+
/** The dropped stream. */
|
|
61
|
+
export function DroppedStream({ rows }) {
|
|
62
|
+
if (rows.length === 0)
|
|
63
|
+
return _jsx("div", { className: "empty", children: "Nothing dropped. Every message this source received was attributed." });
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsxs("p", { className: "stream-note", children: ["Segment answers 200 and discards these; the twin records them. Only ", _jsx("code", { children: "no_user_anon_id" }), " is a reason Segment itself prints \u2014 the rest carry a ", _jsx("span", { className: "drop-reason-twin", children: "twin label" }), "."] }), _jsx("ul", { className: "dropped-stream", children: rows.map((r) => _jsx(DroppedRow, { row: r }, String(r.messageId))) })] }));
|
|
65
|
+
}
|
|
66
|
+
function App() {
|
|
67
|
+
const [stream, setStream] = useState('events');
|
|
68
|
+
const [events, setEvents] = useState([]);
|
|
69
|
+
const [dropped, setDropped] = useState([]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
void (async () => {
|
|
72
|
+
setEvents(await store('events'));
|
|
73
|
+
setDropped(await store('dropped'));
|
|
74
|
+
})();
|
|
75
|
+
}, []);
|
|
76
|
+
return (_jsxs("main", { className: "app", children: [_jsxs("header", { className: "app-header", children: [_jsx("h1", { className: "app-title", children: "Source Debugger" }), _jsxs("span", { className: "counts", children: [_jsxs("span", { className: "count-accepted", "data-count": events.length, children: [events.length, " accepted"] }), _jsxs("span", { className: "count-dropped", "data-count": dropped.length, children: [dropped.length, " dropped"] })] })] }), _jsx("nav", { className: "app-nav", children: STREAMS.map((s) => (_jsx("button", { className: stream === s.key ? 'nav-item nav-item-active' : 'nav-item', onClick: () => setStream(s.key), children: s.label }, s.key))) }), stream === 'events' ? _jsx(EventStream, { rows: events }) : _jsx(DroppedStream, { rows: dropped })] }));
|
|
77
|
+
}
|
|
78
|
+
const mount = typeof document === 'undefined' ? null : document.getElementById('root');
|
|
79
|
+
if (mount)
|
|
80
|
+
createRoot(mount).render(_jsx(App, {}));
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Segment mirror — the SOURCE DEBUGGER, as a React/TSX client. Every byte on screen is fetched
|
|
2
|
+
// from the twin's OWN store door on the same origin (`/twin/store/events`, `/twin/store/dropped`)
|
|
3
|
+
// — the kernel's read-only named-projection door, and the sanctioned way a mirror reads twin
|
|
4
|
+
// state. API↔UI parity: this is the projection the ingest routes fold into, not a second copy.
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
PILL_CLASS,
|
|
9
|
+
clockLabel,
|
|
10
|
+
messageLabel,
|
|
11
|
+
propertyChips,
|
|
12
|
+
subjectOf,
|
|
13
|
+
typeTone,
|
|
14
|
+
type SegmentRow,
|
|
15
|
+
} from '../src/segment-mirror-ui.ts';
|
|
16
|
+
|
|
17
|
+
// WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative, as
|
|
18
|
+
// before); served under a path prefix with a <base> tag every read and write resolves inside that
|
|
19
|
+
// prefix instead of escaping it.
|
|
20
|
+
const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
|
|
21
|
+
|
|
22
|
+
const STREAMS = [
|
|
23
|
+
{ key: 'events', label: 'Accepted' },
|
|
24
|
+
{ key: 'dropped', label: 'Dropped' },
|
|
25
|
+
] as const;
|
|
26
|
+
type StreamKey = (typeof STREAMS)[number]['key'];
|
|
27
|
+
|
|
28
|
+
/** Read one named projection off the twin's store door. */
|
|
29
|
+
export async function store(name: string): Promise<SegmentRow[]> {
|
|
30
|
+
const res = await fetch(`${WIRE_BASE}/twin/store/${name}`);
|
|
31
|
+
if (!res.ok) return [];
|
|
32
|
+
const body = (await res.json()) as unknown;
|
|
33
|
+
return Array.isArray(body) ? (body as SegmentRow[]) : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The property/trait chips on an expanded row — what a debugger reader is actually looking for
|
|
37
|
+
* when an event "arrived but is wrong". */
|
|
38
|
+
export function PropertyChips({ row }: { row: SegmentRow }) {
|
|
39
|
+
const chips = [...propertyChips(row.properties), ...propertyChips(row.traits)];
|
|
40
|
+
if (chips.length === 0) return <div className="chips chips-empty">no properties</div>;
|
|
41
|
+
return (
|
|
42
|
+
<div className="chips">
|
|
43
|
+
{chips.map((c) => (
|
|
44
|
+
<span className="chip" key={c.key}>
|
|
45
|
+
<span className="chip-key">{c.key}</span>
|
|
46
|
+
<span className="chip-value">{c.value}</span>
|
|
47
|
+
</span>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One row of the accepted stream. */
|
|
54
|
+
export function EventRow({ row }: { row: SegmentRow }) {
|
|
55
|
+
const tone = typeTone(row.messageType);
|
|
56
|
+
return (
|
|
57
|
+
<li className="event-row" data-message-id={String(row.messageId ?? '')}>
|
|
58
|
+
<div className="event-head">
|
|
59
|
+
<span className="event-clock">{clockLabel(row.receivedAt)}</span>
|
|
60
|
+
<span className={PILL_CLASS[tone]}>{String(row.messageType ?? '')}</span>
|
|
61
|
+
<span className="event-label">{messageLabel(row)}</span>
|
|
62
|
+
<span className="event-subject">{subjectOf(row)}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<PropertyChips row={row} />
|
|
65
|
+
</li>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One row of the dropped stream.
|
|
71
|
+
*
|
|
72
|
+
* THIS PANE IS THE TWIN'S, NOT THE VENDOR'S, and the screen says so. Segment's real Source
|
|
73
|
+
* Debugger shows what a source RECEIVED; it has no "accepted-then-silently-dropped, with a reason"
|
|
74
|
+
* view, because the vendor answers 200 and discards. This twin records the refusal instead, which
|
|
75
|
+
* is the only way "returned 200 and never arrived" becomes observable — a superset of the vendor's
|
|
76
|
+
* screen, and the honest label for it. Only `no_user_anon_id` is a reason Segment itself prints
|
|
77
|
+
* (its Errors page); the other six are this pack's own descriptive strings, which is why the row
|
|
78
|
+
* marks them.
|
|
79
|
+
*/
|
|
80
|
+
export function DroppedRow({ row }: { row: SegmentRow }) {
|
|
81
|
+
return (
|
|
82
|
+
<li className="dropped-row" data-message-id={String(row.messageId ?? '')}>
|
|
83
|
+
<div className="event-head">
|
|
84
|
+
<span className={PILL_CLASS.dropped}>dropped</span>
|
|
85
|
+
<span className="event-label">{String(row.messageType ?? '')}</span>
|
|
86
|
+
<span className="drop-reason">{String(row.reason ?? 'unknown')}</span>
|
|
87
|
+
{String(row.reason ?? '') === 'no_user_anon_id'
|
|
88
|
+
? null
|
|
89
|
+
: <span className="drop-reason-twin" title="This reason is a twin-authored label. Segment prints only `no_user_anon_id`.">twin label</span>}
|
|
90
|
+
<span className="drop-endpoint">{String(row.endpoint ?? '')}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</li>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The accepted stream, newest last (the order the source received them). */
|
|
97
|
+
export function EventStream({ rows }: { rows: SegmentRow[] }) {
|
|
98
|
+
if (rows.length === 0) return <div className="empty">No events yet. Send one and it appears here.</div>;
|
|
99
|
+
return <ul className="event-stream">{rows.map((r) => <EventRow row={r} key={String(r.messageId)} />)}</ul>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The dropped stream. */
|
|
103
|
+
export function DroppedStream({ rows }: { rows: SegmentRow[] }) {
|
|
104
|
+
if (rows.length === 0) return <div className="empty">Nothing dropped. Every message this source received was attributed.</div>;
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<p className="stream-note">
|
|
108
|
+
Segment answers 200 and discards these; the twin records them. Only <code>no_user_anon_id</code> is
|
|
109
|
+
a reason Segment itself prints — the rest carry a <span className="drop-reason-twin">twin label</span>.
|
|
110
|
+
</p>
|
|
111
|
+
<ul className="dropped-stream">{rows.map((r) => <DroppedRow row={r} key={String(r.messageId)} />)}</ul>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function App() {
|
|
117
|
+
const [stream, setStream] = useState<StreamKey>('events');
|
|
118
|
+
const [events, setEvents] = useState<SegmentRow[]>([]);
|
|
119
|
+
const [dropped, setDropped] = useState<SegmentRow[]>([]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
void (async () => {
|
|
123
|
+
setEvents(await store('events'));
|
|
124
|
+
setDropped(await store('dropped'));
|
|
125
|
+
})();
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<main className="app">
|
|
130
|
+
<header className="app-header">
|
|
131
|
+
<h1 className="app-title">Source Debugger</h1>
|
|
132
|
+
<span className="counts">
|
|
133
|
+
<span className="count-accepted" data-count={events.length}>{events.length} accepted</span>
|
|
134
|
+
<span className="count-dropped" data-count={dropped.length}>{dropped.length} dropped</span>
|
|
135
|
+
</span>
|
|
136
|
+
</header>
|
|
137
|
+
<nav className="app-nav">
|
|
138
|
+
{STREAMS.map((s) => (
|
|
139
|
+
<button
|
|
140
|
+
key={s.key}
|
|
141
|
+
className={stream === s.key ? 'nav-item nav-item-active' : 'nav-item'}
|
|
142
|
+
onClick={() => setStream(s.key)}
|
|
143
|
+
>
|
|
144
|
+
{s.label}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</nav>
|
|
148
|
+
{stream === 'events' ? <EventStream rows={events} /> : <DroppedStream rows={dropped} />}
|
|
149
|
+
</main>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const mount = typeof document === 'undefined' ? null : document.getElementById('root');
|
|
154
|
+
if (mount) createRoot(mount).render(<App />);
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { optionValue } from '@volter/world-core/args';
|
|
3
|
+
import { createSegmentTwinServer } from "./segment-server.js";
|
|
4
|
+
import { OPS } from "./segment-surface.gen.js";
|
|
5
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
6
|
+
if (cmd === 'serve') {
|
|
7
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
8
|
+
const root = optionValue(rest, '--root') || process.env.VOLTER_STATE_DIR;
|
|
9
|
+
const server = await createSegmentTwinServer({ port, root });
|
|
10
|
+
console.log(`segment twin listening on ${server.url}`);
|
|
11
|
+
}
|
|
12
|
+
else if (cmd === 'mirror') {
|
|
13
|
+
// The Source Debugger, and the tracking API behind it, on ONE origin — the screen and its data
|
|
14
|
+
// cannot drift onto different ports.
|
|
15
|
+
const { createSegmentMirrorServer } = await import("./segment-mirror-ui.js");
|
|
16
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
17
|
+
const root = optionValue(rest, '--root') || process.env.VOLTER_STATE_DIR;
|
|
18
|
+
const server = await createSegmentMirrorServer({ port, root });
|
|
19
|
+
console.log(`segment Source Debugger + tracking API listening on http://127.0.0.1:${server.port}`);
|
|
20
|
+
}
|
|
21
|
+
else if (cmd === 'conformance') {
|
|
22
|
+
// Lazily imported so the dev-only conformance module never enters the runtime entrypoint graph.
|
|
23
|
+
const { checkSegmentConformance } = await import("./segment-conformance.js");
|
|
24
|
+
const report = await checkSegmentConformance({ root: process.env.VOLTER_STATE_DIR });
|
|
25
|
+
console.log(JSON.stringify(report, null, 2));
|
|
26
|
+
process.exit(report.ok ? 0 : 1);
|
|
27
|
+
}
|
|
28
|
+
else if (cmd === 'ops') {
|
|
29
|
+
for (const op of OPS)
|
|
30
|
+
console.log(`${op.method.padEnd(6)} ${op.path} [${op.id}]`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log('usage: world-segment serve [--port N] [--root DIR] | mirror [--port N] [--root DIR] | conformance | ops');
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OPS, matchOp } from './segment-surface.gen.js';
|
|
2
|
+
export { SEMANTICS, handleSegmentTwinRequest, events, identities, groups, dropped, ingest, decodeEnvelope, resolveWriteKey, segmentError, MODELED_TYPES, UNMODELED_TYPES, SEGMENT_MAX_REQUEST_BYTES, SEGMENT_MAX_BATCH_BYTES, SEGMENT_MAX_BATCH_EVENTS, } from './segment-twin.js';
|
|
3
|
+
export { createSegmentTwinFetch, createSegmentTwinServer, type SegmentTwinFetchOptions } from './segment-server.js';
|
|
4
|
+
export { createSegmentMirrorServer, buildSegmentMirrorClient, segmentMirrorHtml, clockLabel, messageLabel, propertyChips, subjectOf, typeTone, PILL_CLASS, type PillTone, type SegmentRow, } from './segment-mirror-ui.js';
|
|
5
|
+
export { liveSegmentExecute, syncSegmentFromReal, pushPendingSegmentActions, pushEnvelopeFor, stampEnvelope, SEGMENT_API_BASE } from './segment-connector.js';
|
|
6
|
+
export { SegmentBudget, SEGMENT_RATE_BUDGET, SEGMENT_BUDGET_CEILING, SEGMENT_CALL_WEIGHTS, segmentBudgetPath, segmentCallWeight } from './segment-budget.js';
|
|
7
|
+
import type { TwinPack } from '@volter/world-core';
|
|
8
|
+
export declare const pack: TwinPack;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The pack's RUNTIME surface — what a real app imports. Dev-only tooling (the conformance
|
|
2
|
+
// harness, the capability manifest) is deliberately absent: docs/contributing/architecture.md E2 requires a twin
|
|
3
|
+
// to run without @volter/world-tooling installed, and both of those import it. Reach them
|
|
4
|
+
// through `world-segment conformance` or a direct module import instead.
|
|
5
|
+
// `errorBody` is deliberately NOT re-exported from the generated module: its {success, message}
|
|
6
|
+
// shape is scripts/sdk-compile.ts's scaffold default, and Segment's real error envelope is
|
|
7
|
+
// {code, message} (grounded in analytics-python's own parser). Re-exporting it would advertise a
|
|
8
|
+
// shape this vendor never sends. Use `segmentError` below instead.
|
|
9
|
+
export { OPS, matchOp } from "./segment-surface.gen.js";
|
|
10
|
+
export { SEMANTICS, handleSegmentTwinRequest, events, identities, groups, dropped, ingest, decodeEnvelope, resolveWriteKey, segmentError, MODELED_TYPES, UNMODELED_TYPES, SEGMENT_MAX_REQUEST_BYTES, SEGMENT_MAX_BATCH_BYTES, SEGMENT_MAX_BATCH_EVENTS, } from "./segment-twin.js";
|
|
11
|
+
export { createSegmentTwinFetch, createSegmentTwinServer } from "./segment-server.js";
|
|
12
|
+
export { createSegmentMirrorServer, buildSegmentMirrorClient, segmentMirrorHtml, clockLabel, messageLabel, propertyChips, subjectOf, typeTone, PILL_CLASS, } from "./segment-mirror-ui.js";
|
|
13
|
+
export { liveSegmentExecute, syncSegmentFromReal, pushPendingSegmentActions, pushEnvelopeFor, stampEnvelope, SEGMENT_API_BASE } from "./segment-connector.js";
|
|
14
|
+
export { SegmentBudget, SEGMENT_RATE_BUDGET, SEGMENT_BUDGET_CEILING, SEGMENT_CALL_WEIGHTS, segmentBudgetPath, segmentCallWeight } from "./segment-budget.js";
|
|
15
|
+
import { SEGMENT_RATE_BUDGET as RATE_BUDGET } from "./segment-budget.js";
|
|
16
|
+
export const pack = {
|
|
17
|
+
vendor: 'segment',
|
|
18
|
+
// The SAME object segment-budget.ts declares at module load — one source of truth, so
|
|
19
|
+
// registering the pack and importing the connector can never arm two different ceilings.
|
|
20
|
+
rateBudget: RATE_BUDGET,
|
|
21
|
+
transport: 'rest',
|
|
22
|
+
archetype: 'crud',
|
|
23
|
+
bin: 'world-segment',
|
|
24
|
+
// The subject types the twin stores (segment-twin.ts projections); `dropped` is a real stored
|
|
25
|
+
// row — a message the tracking plane refused, kept rather than discarded.
|
|
26
|
+
resources: ['event', 'identity', 'group', 'dropped'],
|
|
27
|
+
specSource: 'NO first-party machine-readable spec exists for this surface — the denominator was AUTHORED op by ' +
|
|
28
|
+
'op at var/line/segment/SURFACE.json (16 operations, each carrying its evidence) and compiled into ' +
|
|
29
|
+
'segment-surface.gen.ts / segment-capabilities.gen.ts; docs grounding is github.com/segmentio/' +
|
|
30
|
+
'segment-docs, the vendor\'s own documentation source. See spec-sources.json.',
|
|
31
|
+
description: "Segment HTTP Tracking API twin — POST /v1/batch and the direct /v1/{track,identify,page,screen," +
|
|
32
|
+
'group,alias} routes modeled over kernel state, so an unmodified @segment/analytics-node client ' +
|
|
33
|
+
'round-trips offline, plus the Source Debugger mirror: the live accepted/dropped stream this ' +
|
|
34
|
+
'write-only API has no way to read back.',
|
|
35
|
+
// Adoption: the Node tracking client this pack round-trips unmodified, plus the SEGMENT_*
|
|
36
|
+
// credential stem — declared HERE rather than in the central maps (adoption-facts sweep
|
|
37
|
+
// 2026-08-31). `@segment/analytics-next` is deliberately NOT claimed: it is the browser
|
|
38
|
+
// build surface (CDN settings fetch), not this HTTP Tracking API.
|
|
39
|
+
adoption: {
|
|
40
|
+
// Segment's official Python analytics library under both distribution names it has published
|
|
41
|
+
// under (the older `analytics-python` is still pinned widely).
|
|
42
|
+
pypi: ['segment-analytics-python', 'analytics-python'],
|
|
43
|
+
sdks: ['@segment/analytics-node'], envStems: ['SEGMENT'],
|
|
44
|
+
},
|
|
45
|
+
// Interception, moved off the hand VENDOR_HOSTS table unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
46
|
+
// 2026-08-31): the US and EU tracking hosts plus their OAuth token endpoints.
|
|
47
|
+
hosts: [
|
|
48
|
+
{ host: 'api.segment.io' },
|
|
49
|
+
{ host: 'events.eu1.segmentapis.com' },
|
|
50
|
+
{ host: 'oauth2.segment.io' },
|
|
51
|
+
{ host: 'oauth2.eu1.segmentapis.com' },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
|
|
2
|
+
/** Rolling window, in ms. Matches the kernel fallback: no vendor figure licenses a longer one. */
|
|
3
|
+
export declare const SEGMENT_BUDGET_WINDOW_MS = 60000;
|
|
4
|
+
/** Weighted units per window. Deliberately EQUAL to the kernel fallback's ceiling — see header. */
|
|
5
|
+
export declare const SEGMENT_BUDGET_CEILING = 60;
|
|
6
|
+
/** Seconds. A Retry-After above this means the credential is blocked, not throttled — fail loudly. */
|
|
7
|
+
export declare const SEGMENT_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
8
|
+
export declare const SEGMENT_CALL_WEIGHTS: {
|
|
9
|
+
/** Tracking-plane writes: /v1/batch, the mobile /v1/b, analytics.js's /v1/t, the six direct
|
|
10
|
+
* /v1/<type> routes and the six /v1/pixel/* routes. The kernel fallback's default weight. */
|
|
11
|
+
readonly ingestion: 2;
|
|
12
|
+
/** POST /token on oauth2.segment.io — credential minting, with its own unrecoverable-error
|
|
13
|
+
* handling in the SDK's TokenManager. Three times an ingest write. */
|
|
14
|
+
readonly oauth: 6;
|
|
15
|
+
/** Everything this pack has not classified. Equal to the ingestion weight so an UNKNOWN
|
|
16
|
+
* endpoint is never cheaper than a known one — and never free. */
|
|
17
|
+
readonly other: 2;
|
|
18
|
+
};
|
|
19
|
+
/** THE PACK'S DECLARATION — pure data, first-match-wins on `"<METHOD> <pathname>"`. */
|
|
20
|
+
export declare const SEGMENT_RATE_BUDGET: RateBudgetDeclaration;
|
|
21
|
+
/** Price one call, keyed off the request the connector is about to make — an unclassified
|
|
22
|
+
* endpoint still costs `defaultWeight`; an unknown endpoint must never be free. */
|
|
23
|
+
export declare function segmentCallWeight(method: string, path: string): number;
|
|
24
|
+
/** Where Segment's ledger lives. Key-keyed and cwd-independent by default (the allowance is per
|
|
25
|
+
* source write key; a cwd-scoped ledger would hand one credential a fresh allowance per checkout). */
|
|
26
|
+
export declare function segmentBudgetPath(opts?: {
|
|
27
|
+
root?: string;
|
|
28
|
+
token?: string;
|
|
29
|
+
} | string): string;
|
|
30
|
+
export type SegmentBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
31
|
+
/** Segment's budget — the shared kernel guard bound to this vendor's declaration. A real subclass
|
|
32
|
+
* so `SegmentBudget` means "accounts against SEGMENT's ledger under ITS ceiling". It overrides
|
|
33
|
+
* nothing: `assertBudgetGuardIntact` in the connector checks the guard methods are the kernel's
|
|
34
|
+
* own, so an override here would be REFUSED rather than trusted. */
|
|
35
|
+
export declare class SegmentBudget extends RateBudget {
|
|
36
|
+
constructor(opts?: SegmentBudgetOptions);
|
|
37
|
+
}
|
|
38
|
+
export { RateBudgetError as SegmentBudgetError } from '@volter/world-core';
|
|
39
|
+
export type { RateBudgetErrorKind as SegmentBudgetErrorKind } from '@volter/world-core';
|
|
40
|
+
export type SegmentBudgetReservation = RateBudgetReservation;
|
|
41
|
+
export type SegmentBudgetSnapshot = RateBudgetSnapshot;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Segment's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings `liveSegmentExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
|
|
3
|
+
// window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
|
|
4
|
+
// lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → rateBudget.ts). Transcribed from
|
|
5
|
+
// packages/twin/mixpanel/src/mixpanel-budget.ts, the sibling analytics pack; the METHOD is
|
|
6
|
+
// copied, the NUMBERS are this vendor's own.
|
|
7
|
+
//
|
|
8
|
+
// ── HOW THE CEILING WAS CHOSEN ───────────────────────────────────────────────────────────────
|
|
9
|
+
// Unlike Mixpanel, Segment DOES publish scalars. Live-fetched during this build, on 2026-08-24,
|
|
10
|
+
// from the docs' own source repository (github.com/segmentio/segment-docs — segment.com itself
|
|
11
|
+
// answers 403 to non-browser clients, so the rendered page was not read):
|
|
12
|
+
// • src/connections/sources/catalog/libraries/server/http-api/index.md '## Rate limits':
|
|
13
|
+
// "For each workspace, Segment recommends you to not exceed 1,000 requests per second with
|
|
14
|
+
// the HTTP API... Requests that exceed acceptable limits may be rejected with HTTP Status
|
|
15
|
+
// Code 429. When Segment rejects the requests, the response header contains `Retry-After`
|
|
16
|
+
// and `X-RateLimit-Reset` headers".
|
|
17
|
+
// • src/connections/rate-limits.md ("These limits were updated on January 25, 2024"): the same
|
|
18
|
+
// 1,000-per-second inbound ingestion figure, plus 10,000 properties per individual event.
|
|
19
|
+
// • '## Max request size': 32KB per normal request, 500KB per batch request, 32KB per event in
|
|
20
|
+
// a batch, and (from '## Errors') 2,500 events per batch request.
|
|
21
|
+
//
|
|
22
|
+
// 1,000 req/s is NOT adopted as this ceiling, and that is the whole judgement. It is a
|
|
23
|
+
// WORKSPACE-WIDE production-throughput recommendation for an entire company's live traffic — the
|
|
24
|
+
// number above which Segment "reserves the right to queue" — not an allowance handed to one
|
|
25
|
+
// connector. This pack's only live caller is a REPLAY path (pushPendingSegmentActions), so
|
|
26
|
+
// spending a production firehose's allowance from a dev machine would be dressing a vendor
|
|
27
|
+
// recommendation up as a client budget. The declaration therefore claims NO extra permissiveness
|
|
28
|
+
// over the kernel fallback: 60 weighted units per 60s at defaultWeight 2 — 30 calls/minute, the
|
|
29
|
+
// fallback's exact shape — which is also why no VENDOR_BURST_ANCHOR is owed (that obligation
|
|
30
|
+
// binds only a declaration that out-bursts the fallback). An operator who knows their own plan
|
|
31
|
+
// can only TIGHTEN from here.
|
|
32
|
+
//
|
|
33
|
+
// ── HOW THE WEIGHTS WERE CHOSEN ──────────────────────────────────────────────────────────────
|
|
34
|
+
// Never cheaper than the default — a discount claims allowance no published figure licenses.
|
|
35
|
+
// Ingestion writes sit at the default 2. The OAuth token exchange costs 6 because it is a
|
|
36
|
+
// CREDENTIAL-MINTING call on a different host with its own 429 handling (the SDK's TokenManager
|
|
37
|
+
// treats 400/401/415 there as unrecoverable and STOPS its poller), so a runaway auth loop must
|
|
38
|
+
// halt three times sooner than a runaway ingest loop. That 2:6 ratio is a judgement about
|
|
39
|
+
// relative risk, not a published cost, and is stated as such.
|
|
40
|
+
import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, } from '@volter/world-core';
|
|
41
|
+
const VENDOR = 'segment';
|
|
42
|
+
/** Rolling window, in ms. Matches the kernel fallback: no vendor figure licenses a longer one. */
|
|
43
|
+
export const SEGMENT_BUDGET_WINDOW_MS = 60_000;
|
|
44
|
+
/** Weighted units per window. Deliberately EQUAL to the kernel fallback's ceiling — see header. */
|
|
45
|
+
export const SEGMENT_BUDGET_CEILING = 60;
|
|
46
|
+
/** Seconds. A Retry-After above this means the credential is blocked, not throttled — fail loudly. */
|
|
47
|
+
export const SEGMENT_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
48
|
+
export const SEGMENT_CALL_WEIGHTS = {
|
|
49
|
+
/** Tracking-plane writes: /v1/batch, the mobile /v1/b, analytics.js's /v1/t, the six direct
|
|
50
|
+
* /v1/<type> routes and the six /v1/pixel/* routes. The kernel fallback's default weight. */
|
|
51
|
+
ingestion: 2,
|
|
52
|
+
/** POST /token on oauth2.segment.io — credential minting, with its own unrecoverable-error
|
|
53
|
+
* handling in the SDK's TokenManager. Three times an ingest write. */
|
|
54
|
+
oauth: 6,
|
|
55
|
+
/** Everything this pack has not classified. Equal to the ingestion weight so an UNKNOWN
|
|
56
|
+
* endpoint is never cheaper than a known one — and never free. */
|
|
57
|
+
other: 2,
|
|
58
|
+
};
|
|
59
|
+
/** THE PACK'S DECLARATION — pure data, first-match-wins on `"<METHOD> <pathname>"`. */
|
|
60
|
+
export const SEGMENT_RATE_BUDGET = {
|
|
61
|
+
windowMs: SEGMENT_BUDGET_WINDOW_MS,
|
|
62
|
+
ceiling: SEGMENT_BUDGET_CEILING,
|
|
63
|
+
defaultWeight: SEGMENT_CALL_WEIGHTS.other,
|
|
64
|
+
maxRetryAfterSeconds: SEGMENT_BUDGET_MAX_RETRY_AFTER_S,
|
|
65
|
+
rules: [
|
|
66
|
+
{ match: '^POST /v1/(batch|b|t)$', weight: SEGMENT_CALL_WEIGHTS.ingestion },
|
|
67
|
+
{ match: '^POST /v1/(track|identify|page|screen|group|alias)$', weight: SEGMENT_CALL_WEIGHTS.ingestion },
|
|
68
|
+
{ match: '^GET /v1/pixel/(track|identify|page|screen|group|alias)$', weight: SEGMENT_CALL_WEIGHTS.ingestion },
|
|
69
|
+
{ match: '^POST /token$', weight: SEGMENT_CALL_WEIGHTS.oauth },
|
|
70
|
+
],
|
|
71
|
+
reason: "Segment publishes scalar limits and this declaration deliberately does NOT adopt the headline one. " +
|
|
72
|
+
"Live-fetched 2026-08-24 from github.com/segmentio/segment-docs (the docs' own source; segment.com " +
|
|
73
|
+
'answers 403 to non-browser clients): src/connections/sources/catalog/libraries/server/http-api/index.md ' +
|
|
74
|
+
"'## Rate limits' — \"For each workspace, Segment recommends you to not exceed 1,000 requests per second " +
|
|
75
|
+
'with the HTTP API... Requests that exceed acceptable limits may be rejected with HTTP Status Code 429. ' +
|
|
76
|
+
'When Segment rejects the requests, the response header contains Retry-After and X-RateLimit-Reset headers"; ' +
|
|
77
|
+
'src/connections/rate-limits.md (updated January 25, 2024) repeats the 1,000/second inbound figure. That ' +
|
|
78
|
+
'number is a WORKSPACE-WIDE production-throughput recommendation, not a per-connector allowance, and this ' +
|
|
79
|
+
"pack's only live caller is a replay path — so adopting it would dress a vendor recommendation up as a " +
|
|
80
|
+
'client budget. The ceiling is therefore exactly the kernel fallback shape: 60 weighted units / 60s at ' +
|
|
81
|
+
'defaultWeight 2 (30 calls/minute, same burst), claiming no extra permissiveness and owing no burst anchor. ' +
|
|
82
|
+
"The vendor's cost STRUCTURE appears only in the relative weights, all at or above the default — tracking " +
|
|
83
|
+
'writes 2, the OAuth token exchange 6 — so a runaway auth loop halts three times sooner than a runaway ' +
|
|
84
|
+
'ingest loop. That 2:6 ratio is a judgement about relative risk, not a published cost. Size limits from the ' +
|
|
85
|
+
"same page ('## Max request size': 32KB per request, 500KB per batch, 32KB per batched event; '## Errors': " +
|
|
86
|
+
'2,500 events per batch) are enforced in segment-twin.ts, not here — they bound payloads, not call rate.',
|
|
87
|
+
};
|
|
88
|
+
// Declared at module load: importing this module (which segment-connector.ts does) arms the ceiling.
|
|
89
|
+
declareRateBudget(VENDOR, SEGMENT_RATE_BUDGET);
|
|
90
|
+
/** Price one call, keyed off the request the connector is about to make — an unclassified
|
|
91
|
+
* endpoint still costs `defaultWeight`; an unknown endpoint must never be free. */
|
|
92
|
+
export function segmentCallWeight(method, path) {
|
|
93
|
+
const q = path.indexOf('?');
|
|
94
|
+
const pathname = q < 0 ? path : path.slice(0, q);
|
|
95
|
+
return rateBudgetWeight(VENDOR, `${(method || 'GET').toUpperCase()} ${pathname}`, {});
|
|
96
|
+
}
|
|
97
|
+
/** Where Segment's ledger lives. Key-keyed and cwd-independent by default (the allowance is per
|
|
98
|
+
* source write key; a cwd-scoped ledger would hand one credential a fresh allowance per checkout). */
|
|
99
|
+
export function segmentBudgetPath(opts = {}) {
|
|
100
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
101
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
102
|
+
}
|
|
103
|
+
/** Segment's budget — the shared kernel guard bound to this vendor's declaration. A real subclass
|
|
104
|
+
* so `SegmentBudget` means "accounts against SEGMENT's ledger under ITS ceiling". It overrides
|
|
105
|
+
* nothing: `assertBudgetGuardIntact` in the connector checks the guard methods are the kernel's
|
|
106
|
+
* own, so an override here would be REFUSED rather than trusted. */
|
|
107
|
+
export class SegmentBudget extends RateBudget {
|
|
108
|
+
constructor(opts = {}) {
|
|
109
|
+
super({ ...opts, vendor: VENDOR });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export { RateBudgetError as SegmentBudgetError } from '@volter/world-core';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
|
|
2
|
+
export declare const SEGMENT_CAPABILITIES: CapabilitySpec[];
|
|
3
|
+
/** Committed area census (TWIN-87/F1), enumerated TOP-DOWN from the vendor's own documentation
|
|
4
|
+
* nav rather than from this manifest. The nine op areas are the ones SURFACE.json ratified
|
|
5
|
+
* (batch, track, identify, page, screen, group, alias, pixel, oauth); `auth`, `common`, `errors`,
|
|
6
|
+
* `limits` and `regional` are the H2 sections of the vendor's HTTP Tracking API page that carry
|
|
7
|
+
* behaviour rather than routes ('### Authentication', the Spec's common fields, '## Errors',
|
|
8
|
+
* '## Rate limits' + '## Max request size', '## Regional configuration'); `conformance`,
|
|
9
|
+
* `connector` and `mirror` are this pack's structural areas. A regeneration that DROPS an area
|
|
10
|
+
* reddens the census test instead of silently shrinking the denominator. */
|
|
11
|
+
export declare const SEGMENT_AREAS: readonly ["alias", "auth", "batch", "common", "conformance", "connector", "errors", "group", "identify", "limits", "mirror", "oauth", "page", "pixel", "regional", "screen", "track"];
|
|
12
|
+
export declare function segmentCapabilities(): Promise<CapabilityReport>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// GENERATED by scripts/sdk-compile.ts from var/line/segment/SURFACE.json — the vendor
|
|
2
|
+
// publishes no machine-readable spec; every op below carries the evidence it was ratified on.
|
|
3
|
+
// Data only; regenerate, never hand-edit.
|
|
4
|
+
/** Every ratified op, born `todo` — flips only by hand with a real verify. */
|
|
5
|
+
export const GENERATED_CAPABILITIES = [
|
|
6
|
+
{ id: 'segment.api.alias', area: "alias", title: "POST /v1/alias — associate one identity with another: a `previousId` (userId or anonymousId) is aliased onto a new `userId` [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "common" },
|
|
7
|
+
{ id: 'segment.api.batch', area: "batch", title: "POST /v1/batch — the SERVER-side batched ingestion envelope every official server SDK sends: {batch:[<message>...], writeKey, sentAt}, each message discriminated by its own `type` [grounding: SDK WIRE LITERAL. @segment/analytics-node@2.3.0 src/plugins/segmentio/publisher.ts:73-76 builds the ]", dimension: 'api', expected: 'todo', tier: "core" },
|
|
8
|
+
{ id: 'segment.api.browser_track', area: "track", title: "POST /v1/t — analytics.js's short-form tracking route on the api.segment.io/v1 base — the path a browser proxy must forward [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
9
|
+
{ id: 'segment.api.group', area: "group", title: "POST /v1/group — associate an identified user with a group (company/account/team) and record group traits [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "core" },
|
|
10
|
+
{ id: 'segment.api.identify', area: "identify", title: "POST /v1/identify — tie a user to their actions and record `traits` about them [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "core" },
|
|
11
|
+
{ id: 'segment.api.mobile_batch', area: "batch", title: "POST /v1/b — the MOBILE batch endpoint — a distinct route from /v1/batch, which the vendor reserves for server-side sending [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "common" },
|
|
12
|
+
{ id: 'segment.api.oauth_token', area: "oauth", title: "POST /token — OAuth 2.0 client-credentials token exchange on the regional authorization server: a signed RS256 JWT client assertion is traded for a short-lived access token [grounding: SDK WIRE LITERAL (interpolated, which is why wire.json's plain-string grep missed it). @segment/anal]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
13
|
+
{ id: 'segment.api.page', area: "page", title: "POST /v1/page — record a web page view, with optional category, name and properties [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "core" },
|
|
14
|
+
{ id: 'segment.api.pixel_alias', area: "pixel", title: "GET /v1/pixel/alias — Tracking Pixel API: an alias call carried in the query string, answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
15
|
+
{ id: 'segment.api.pixel_group', area: "pixel", title: "GET /v1/pixel/group — Tracking Pixel API: a group call carried in the query string, answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
16
|
+
{ id: 'segment.api.pixel_identify', area: "pixel", title: "GET /v1/pixel/identify — Tracking Pixel API: an identify call carried in the query string, answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
17
|
+
{ id: 'segment.api.pixel_page', area: "pixel", title: "GET /v1/pixel/page — Tracking Pixel API: a page call carried in the query string, answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
18
|
+
{ id: 'segment.api.pixel_screen', area: "pixel", title: "GET /v1/pixel/screen — Tracking Pixel API: a screen call carried in the query string, answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
19
|
+
{ id: 'segment.api.pixel_track', area: "pixel", title: "GET /v1/pixel/track — Tracking Pixel API: a track call carried in the query string (base64 `?data=` or plain params), answering 200 with a 1x1 transparent GIF [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "niche" },
|
|
20
|
+
{ id: 'segment.api.screen', area: "screen", title: "POST /v1/screen — record a mobile app screen view, with optional category, name and properties [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "common" },
|
|
21
|
+
{ id: 'segment.api.track', area: "track", title: "POST /v1/track — record an action a user performed: required `event` name plus optional `properties` [grounding: DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/l]", dimension: 'api', expected: 'todo', tier: "core" },
|
|
22
|
+
];
|