@reproapp/node-sdk 0.0.38 → 0.0.39
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 +225 -310
- package/dist/index.d.ts +42 -0
- package/dist/index.js +447 -20
- package/package.json +2 -2
- package/src/index.ts +670 -20
- package/test/backend-live-mode.test.js +403 -0
package/dist/index.js
CHANGED
|
@@ -305,6 +305,9 @@ const KAFKA_HEADER_SESSION_ID = 'x-bug-session-id';
|
|
|
305
305
|
const KAFKA_HEADER_ACTION_ID = 'x-bug-action-id';
|
|
306
306
|
const DEFAULT_INGEST_BASE = 'http://localhost:8080';
|
|
307
307
|
const DEFAULT_INGEST_PATH = '/v1/ingest/events';
|
|
308
|
+
const DEFAULT_LIVE_SESSION_DURATION_MS = 3 * 60000;
|
|
309
|
+
const DEFAULT_LIVE_SEGMENT_IDLE_MS = 15000;
|
|
310
|
+
const DEFAULT_LIVE_SEGMENT_DURATION_MS = DEFAULT_LIVE_SESSION_DURATION_MS;
|
|
308
311
|
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
309
312
|
let __TRACER__ = null;
|
|
310
313
|
let __TRACER_READY = false;
|
|
@@ -1681,6 +1684,299 @@ function toIngestConfig(cfg) {
|
|
|
1681
1684
|
schemaVersion: cfg.schemaVersion,
|
|
1682
1685
|
};
|
|
1683
1686
|
}
|
|
1687
|
+
function normalizeLiveModeConfig(cfg) {
|
|
1688
|
+
const liveMode = cfg.liveMode;
|
|
1689
|
+
if (liveMode === true) {
|
|
1690
|
+
return { enabled: true };
|
|
1691
|
+
}
|
|
1692
|
+
if (!liveMode || typeof liveMode !== 'object') {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
if (liveMode.enabled === false) {
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
return liveMode;
|
|
1699
|
+
}
|
|
1700
|
+
let backendLiveTimingOverrideForTest = null;
|
|
1701
|
+
function positiveInternalDuration(value, fallback) {
|
|
1702
|
+
const numeric = Number(value);
|
|
1703
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
1704
|
+
return fallback;
|
|
1705
|
+
return Math.max(1, Math.trunc(numeric));
|
|
1706
|
+
}
|
|
1707
|
+
function getBackendLiveTiming() {
|
|
1708
|
+
return {
|
|
1709
|
+
sessionMaxDurationMs: positiveInternalDuration(backendLiveTimingOverrideForTest?.sessionMaxDurationMs, DEFAULT_LIVE_SESSION_DURATION_MS),
|
|
1710
|
+
segmentMaxDurationMs: positiveInternalDuration(backendLiveTimingOverrideForTest?.segmentMaxDurationMs, DEFAULT_LIVE_SEGMENT_DURATION_MS),
|
|
1711
|
+
segmentIdleMs: positiveInternalDuration(backendLiveTimingOverrideForTest?.segmentIdleMs, DEFAULT_LIVE_SEGMENT_IDLE_MS),
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
function normalizeLiveActorId(value) {
|
|
1715
|
+
if (value === undefined || value === null)
|
|
1716
|
+
return null;
|
|
1717
|
+
const normalized = String(value).trim();
|
|
1718
|
+
return normalized.length ? normalized : null;
|
|
1719
|
+
}
|
|
1720
|
+
function normalizeLiveActorLabels(value) {
|
|
1721
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1722
|
+
return undefined;
|
|
1723
|
+
const labels = {};
|
|
1724
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
1725
|
+
const normalizedKey = key.trim();
|
|
1726
|
+
if (!normalizedKey || rawValue === undefined || rawValue === null)
|
|
1727
|
+
continue;
|
|
1728
|
+
const normalizedValue = String(rawValue).trim();
|
|
1729
|
+
if (normalizedValue)
|
|
1730
|
+
labels[normalizedKey] = normalizedValue;
|
|
1731
|
+
}
|
|
1732
|
+
return Object.keys(labels).length ? labels : undefined;
|
|
1733
|
+
}
|
|
1734
|
+
function newSessionId(base = Date.now()) {
|
|
1735
|
+
return `S_${Math.max(0, Math.trunc(base))}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
1736
|
+
}
|
|
1737
|
+
function newActionId(base = Date.now()) {
|
|
1738
|
+
return `A_${Math.max(0, Math.trunc(base))}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
1739
|
+
}
|
|
1740
|
+
function resolveLiveApiBase(cfg, liveMode) {
|
|
1741
|
+
const env = typeof process !== 'undefined' ? process?.env : undefined;
|
|
1742
|
+
const raw = liveMode?.apiBase ||
|
|
1743
|
+
cfg.apiBase ||
|
|
1744
|
+
env?.REPRO_API_BASE_URL ||
|
|
1745
|
+
env?.REPRO_API_BASE ||
|
|
1746
|
+
cfg.privacy?.apiBase ||
|
|
1747
|
+
null;
|
|
1748
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
1749
|
+
return null;
|
|
1750
|
+
return raw.trim().replace(/\/+$/, '');
|
|
1751
|
+
}
|
|
1752
|
+
function liveApiHeaders(cfg) {
|
|
1753
|
+
const headers = {
|
|
1754
|
+
'Content-Type': 'application/json',
|
|
1755
|
+
'X-App-Id': cfg.appId,
|
|
1756
|
+
};
|
|
1757
|
+
if (cfg.appSecret) {
|
|
1758
|
+
headers['X-App-Secret'] = cfg.appSecret;
|
|
1759
|
+
}
|
|
1760
|
+
if (cfg.appName) {
|
|
1761
|
+
headers['X-App-Name'] = cfg.appName;
|
|
1762
|
+
}
|
|
1763
|
+
return headers;
|
|
1764
|
+
}
|
|
1765
|
+
async function postLiveApi(cfg, liveMode, path, body) {
|
|
1766
|
+
await postLiveApiJson(cfg, liveMode, path, body);
|
|
1767
|
+
}
|
|
1768
|
+
async function postLiveApiJson(cfg, liveMode, path, body) {
|
|
1769
|
+
const apiBase = resolveLiveApiBase(cfg, liveMode);
|
|
1770
|
+
const fetchImpl = globalThis.fetch;
|
|
1771
|
+
if (!apiBase || !cfg.appId || !cfg.appSecret || typeof fetchImpl !== 'function') {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
const res = await fetchImpl(`${apiBase}${path}`, {
|
|
1776
|
+
method: 'POST',
|
|
1777
|
+
headers: liveApiHeaders(cfg),
|
|
1778
|
+
body: JSON.stringify(body),
|
|
1779
|
+
});
|
|
1780
|
+
if (!res?.ok) {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
return await res.json();
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
// never break user code
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function enqueueLiveSessionStart(cfg, liveMode, payload) {
|
|
1796
|
+
scheduleSdkBackgroundWork(() => postLiveApi(cfg, liveMode, '/v1/live-sessions', payload), { priority: 'high' });
|
|
1797
|
+
}
|
|
1798
|
+
function enqueueLiveSessionFinish(cfg, liveMode, sessionId) {
|
|
1799
|
+
scheduleSdkBackgroundWork(() => postLiveApi(cfg, liveMode, `/v1/live-sessions/${encodeURIComponent(sessionId)}/finish`, { notes: '' }), { priority: 'high' });
|
|
1800
|
+
}
|
|
1801
|
+
function emitBackendLiveSessionManifest(cfg, session, options) {
|
|
1802
|
+
post(cfg, session.sessionId, {
|
|
1803
|
+
entries: [{
|
|
1804
|
+
sessionCaptureManifest: {
|
|
1805
|
+
mode: 'backend-only',
|
|
1806
|
+
captureMode: 'backend-only',
|
|
1807
|
+
sessionKind: options.sessionKind,
|
|
1808
|
+
parentSessionId: options.parentSessionId ?? null,
|
|
1809
|
+
previousSessionId: options.previousSessionId ?? null,
|
|
1810
|
+
actorId: options.actorId,
|
|
1811
|
+
actorType: options.actorType,
|
|
1812
|
+
actorLabels: options.actorLabels,
|
|
1813
|
+
expected: {
|
|
1814
|
+
frontendActionDoc: 0,
|
|
1815
|
+
rrwebChunk: 0,
|
|
1816
|
+
frontendSessionEnd: 0,
|
|
1817
|
+
},
|
|
1818
|
+
closedAt: options.closedAt,
|
|
1819
|
+
},
|
|
1820
|
+
t: options.closedAt,
|
|
1821
|
+
}],
|
|
1822
|
+
}, { priority: 'high' });
|
|
1823
|
+
}
|
|
1824
|
+
function closeBackendLiveParent(cfg, liveMode, state, parent, closedAt) {
|
|
1825
|
+
if (parent.closed) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
parent.closed = true;
|
|
1829
|
+
if (parent.closeTimer) {
|
|
1830
|
+
try {
|
|
1831
|
+
clearTimeout(parent.closeTimer);
|
|
1832
|
+
}
|
|
1833
|
+
catch { }
|
|
1834
|
+
parent.closeTimer = null;
|
|
1835
|
+
}
|
|
1836
|
+
state.previousParentSessionId = parent.sessionId;
|
|
1837
|
+
if (state.parent === parent) {
|
|
1838
|
+
state.parent = null;
|
|
1839
|
+
}
|
|
1840
|
+
emitBackendLiveSessionManifest(cfg, parent, {
|
|
1841
|
+
sessionKind: 'parent',
|
|
1842
|
+
actorId: parent.actorId,
|
|
1843
|
+
actorType: parent.actorType,
|
|
1844
|
+
actorLabels: parent.actorLabels,
|
|
1845
|
+
previousSessionId: parent.previousSessionId,
|
|
1846
|
+
closedAt,
|
|
1847
|
+
});
|
|
1848
|
+
enqueueLiveSessionFinish(cfg, liveMode, parent.sessionId);
|
|
1849
|
+
}
|
|
1850
|
+
function scheduleBackendLiveParentClose(cfg, liveMode, state, parent) {
|
|
1851
|
+
if (parent.closed) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (parent.closeTimer) {
|
|
1855
|
+
try {
|
|
1856
|
+
clearTimeout(parent.closeTimer);
|
|
1857
|
+
}
|
|
1858
|
+
catch { }
|
|
1859
|
+
parent.closeTimer = null;
|
|
1860
|
+
}
|
|
1861
|
+
const now = Date.now();
|
|
1862
|
+
const activeChild = state.activeChild;
|
|
1863
|
+
const activeChildBelongsToParent = activeChild?.parentSessionId === parent.sessionId;
|
|
1864
|
+
const closeAt = activeChildBelongsToParent && now >= parent.expiresAt
|
|
1865
|
+
? Math.max(parent.expiresAt, activeChild.closeDeadline, now + 1)
|
|
1866
|
+
: parent.expiresAt;
|
|
1867
|
+
parent.closeTimer = setTimeout(() => {
|
|
1868
|
+
parent.closeTimer = null;
|
|
1869
|
+
if (parent.closed || state.parent !== parent) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const currentChild = state.activeChild;
|
|
1873
|
+
if (currentChild?.parentSessionId === parent.sessionId) {
|
|
1874
|
+
if (currentChild.requestsInFlight <= 0) {
|
|
1875
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, currentChild);
|
|
1876
|
+
}
|
|
1877
|
+
scheduleBackendLiveParentClose(cfg, liveMode, state, parent);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
closeBackendLiveParent(cfg, liveMode, state, parent, Date.now());
|
|
1881
|
+
}, Math.max(0, closeAt - now));
|
|
1882
|
+
try {
|
|
1883
|
+
parent.closeTimer.unref?.();
|
|
1884
|
+
}
|
|
1885
|
+
catch { }
|
|
1886
|
+
}
|
|
1887
|
+
function scheduleBackendLiveChildClose(cfg, liveMode, state, child) {
|
|
1888
|
+
if (child.closeTimer) {
|
|
1889
|
+
try {
|
|
1890
|
+
clearTimeout(child.closeTimer);
|
|
1891
|
+
}
|
|
1892
|
+
catch { }
|
|
1893
|
+
child.closeTimer = null;
|
|
1894
|
+
}
|
|
1895
|
+
const delay = Math.max(0, child.closeDeadline - Date.now());
|
|
1896
|
+
child.closeTimer = setTimeout(() => {
|
|
1897
|
+
child.closeTimer = null;
|
|
1898
|
+
if (child.requestsInFlight > 0 || Date.now() < child.closeDeadline) {
|
|
1899
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, child);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (state.activeChild === child) {
|
|
1903
|
+
state.activeChild = null;
|
|
1904
|
+
}
|
|
1905
|
+
state.previousChildSessionId = child.sessionId;
|
|
1906
|
+
const closedAt = Date.now();
|
|
1907
|
+
emitBackendLiveSessionManifest(cfg, child, {
|
|
1908
|
+
sessionKind: 'child',
|
|
1909
|
+
actorId: child.actorId,
|
|
1910
|
+
actorType: child.actorType,
|
|
1911
|
+
actorLabels: child.actorLabels,
|
|
1912
|
+
parentSessionId: child.parentSessionId,
|
|
1913
|
+
previousSessionId: child.previousSessionId,
|
|
1914
|
+
closedAt,
|
|
1915
|
+
});
|
|
1916
|
+
enqueueLiveSessionFinish(cfg, liveMode, child.sessionId);
|
|
1917
|
+
}, delay);
|
|
1918
|
+
try {
|
|
1919
|
+
child.closeTimer.unref?.();
|
|
1920
|
+
}
|
|
1921
|
+
catch { }
|
|
1922
|
+
}
|
|
1923
|
+
function releaseBackendLiveRequest(cfg, liveMode, state, child, finishedAt) {
|
|
1924
|
+
child.requestsInFlight = Math.max(0, child.requestsInFlight - 1);
|
|
1925
|
+
const idleMs = getBackendLiveTiming().segmentIdleMs;
|
|
1926
|
+
child.closeDeadline = Math.max(child.closeDeadline, finishedAt + idleMs);
|
|
1927
|
+
if (child.requestsInFlight <= 0) {
|
|
1928
|
+
scheduleBackendLiveChildClose(cfg, liveMode, state, child);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
async function resolveBackendLiveContext(cfg, req, requestStart) {
|
|
1932
|
+
const liveMode = normalizeLiveModeConfig(cfg);
|
|
1933
|
+
if (!liveMode?.backendOnly) {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1937
|
+
if (method === 'OPTIONS' && !hasReproRequestCorrelation(req)) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
const identity = await resolveLiveActorIdentity(cfg, req);
|
|
1941
|
+
if (!identity) {
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
const { actorId, actorType, actorLabels } = identity;
|
|
1945
|
+
const claim = await postLiveApiJson(cfg, liveMode, '/v1/live-sessions/claim', {
|
|
1946
|
+
clientTime: requestStart,
|
|
1947
|
+
captureMode: 'backend-only',
|
|
1948
|
+
actorId,
|
|
1949
|
+
actorIds: [actorId],
|
|
1950
|
+
actorType,
|
|
1951
|
+
actorLabels,
|
|
1952
|
+
});
|
|
1953
|
+
const sid = normalizeLiveActorId(claim?.childSessionId) ??
|
|
1954
|
+
normalizeLiveActorId(claim?.sessionId);
|
|
1955
|
+
if (!sid) {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
const aid = normalizeLiveActorId(claim?.actionId) ?? newActionId(requestStart);
|
|
1959
|
+
const parentSessionId = normalizeLiveActorId(claim?.parentSessionId) ?? '';
|
|
1960
|
+
const liveChild = {
|
|
1961
|
+
sessionId: sid,
|
|
1962
|
+
parentSessionId,
|
|
1963
|
+
previousSessionId: null,
|
|
1964
|
+
actorId,
|
|
1965
|
+
actorType,
|
|
1966
|
+
actorLabels,
|
|
1967
|
+
startedAt: requestStart,
|
|
1968
|
+
expiresAt: requestStart,
|
|
1969
|
+
closeDeadline: requestStart,
|
|
1970
|
+
requestsInFlight: 0,
|
|
1971
|
+
closeTimer: null,
|
|
1972
|
+
};
|
|
1973
|
+
return {
|
|
1974
|
+
sid: liveChild.sessionId,
|
|
1975
|
+
aid,
|
|
1976
|
+
requestStart,
|
|
1977
|
+
child: liveChild,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1684
1980
|
function resolveIngestUrl(cfg) {
|
|
1685
1981
|
const envBase = typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_BASE : undefined;
|
|
1686
1982
|
const ingestBase = String(envBase || cfg.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
@@ -1727,6 +2023,19 @@ function readRequestQueryNumber(req, name) {
|
|
|
1727
2023
|
const parsed = Number(raw);
|
|
1728
2024
|
return Number.isFinite(parsed) ? parsed : null;
|
|
1729
2025
|
}
|
|
2026
|
+
function hasReproRequestCorrelation(req) {
|
|
2027
|
+
const headers = req.headers || {};
|
|
2028
|
+
if (hasHttpHeader(headers, KAFKA_HEADER_SESSION_ID) ||
|
|
2029
|
+
hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID) ||
|
|
2030
|
+
hasHttpHeader(headers, KAFKA_HEADER_REQUEST_ID) ||
|
|
2031
|
+
hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID) ||
|
|
2032
|
+
hasHttpHeader(headers, KAFKA_HEADER_SPAN_ID) ||
|
|
2033
|
+
hasHttpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID)) {
|
|
2034
|
+
return true;
|
|
2035
|
+
}
|
|
2036
|
+
return Boolean(readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM) ||
|
|
2037
|
+
readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM));
|
|
2038
|
+
}
|
|
1730
2039
|
function stripReproQueryFromUrl(rawUrl) {
|
|
1731
2040
|
if (!rawUrl || !rawUrl.includes('?')) {
|
|
1732
2041
|
return rawUrl;
|
|
@@ -4131,6 +4440,72 @@ async function maybeCaptureDbValueFromCandidatesAsync(params, sink) {
|
|
|
4131
4440
|
}
|
|
4132
4441
|
return skippedRef;
|
|
4133
4442
|
}
|
|
4443
|
+
function buildLiveActorArgs(cfg, req) {
|
|
4444
|
+
const rawUrl = String(req.originalUrl || req.url || '/');
|
|
4445
|
+
const path = rawUrl.split('?')[0] || '/';
|
|
4446
|
+
return {
|
|
4447
|
+
req,
|
|
4448
|
+
headers: req.headers,
|
|
4449
|
+
method: String(req.method || 'GET').toUpperCase(),
|
|
4450
|
+
url: rawUrl,
|
|
4451
|
+
path,
|
|
4452
|
+
ip: req.ip ?? req.socket?.remoteAddress ?? null,
|
|
4453
|
+
appId: cfg.appId,
|
|
4454
|
+
appName: cfg.appName,
|
|
4455
|
+
serviceName: cfg.serviceName,
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
async function resolveLiveActorIdentity(cfg, req) {
|
|
4459
|
+
const liveMode = normalizeLiveModeConfig(cfg);
|
|
4460
|
+
if (!liveMode) {
|
|
4461
|
+
return null;
|
|
4462
|
+
}
|
|
4463
|
+
const args = buildLiveActorArgs(cfg, req);
|
|
4464
|
+
let rawActorId;
|
|
4465
|
+
try {
|
|
4466
|
+
rawActorId = liveMode.defineActorId
|
|
4467
|
+
? await liveMode.defineActorId(args)
|
|
4468
|
+
: cfg.actorId;
|
|
4469
|
+
}
|
|
4470
|
+
catch {
|
|
4471
|
+
rawActorId = null;
|
|
4472
|
+
}
|
|
4473
|
+
const actorId = normalizeLiveActorId(rawActorId);
|
|
4474
|
+
if (!actorId) {
|
|
4475
|
+
return null;
|
|
4476
|
+
}
|
|
4477
|
+
const actorType = normalizeLiveActorId(liveMode.actorType) ?? cfg.actorType ?? 'user';
|
|
4478
|
+
const decision = {
|
|
4479
|
+
...args,
|
|
4480
|
+
actorId,
|
|
4481
|
+
actorType,
|
|
4482
|
+
};
|
|
4483
|
+
if (liveMode.shouldCapture) {
|
|
4484
|
+
let shouldCapture = false;
|
|
4485
|
+
try {
|
|
4486
|
+
shouldCapture = (await liveMode.shouldCapture(decision)) === true;
|
|
4487
|
+
}
|
|
4488
|
+
catch {
|
|
4489
|
+
shouldCapture = false;
|
|
4490
|
+
}
|
|
4491
|
+
if (!shouldCapture) {
|
|
4492
|
+
return null;
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
let rawLabels;
|
|
4496
|
+
try {
|
|
4497
|
+
rawLabels =
|
|
4498
|
+
typeof liveMode.actorLabels === 'function'
|
|
4499
|
+
? await liveMode.actorLabels(decision)
|
|
4500
|
+
: liveMode.actorLabels;
|
|
4501
|
+
}
|
|
4502
|
+
catch {
|
|
4503
|
+
rawLabels = undefined;
|
|
4504
|
+
}
|
|
4505
|
+
const actorLabels = normalizeLiveActorLabels(rawLabels) ?? normalizeLiveActorLabels(cfg.actorLabels);
|
|
4506
|
+
return { actorId, actorType, actorLabels };
|
|
4507
|
+
}
|
|
4508
|
+
const backendLiveActors = new Map();
|
|
4134
4509
|
function reproMiddleware(cfg) {
|
|
4135
4510
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4136
4511
|
patchMongoDb(cfg);
|
|
@@ -4165,18 +4540,62 @@ function reproMiddleware(cfg) {
|
|
|
4165
4540
|
clientRequestEnded = true;
|
|
4166
4541
|
endClientRequest();
|
|
4167
4542
|
};
|
|
4168
|
-
const
|
|
4169
|
-
const
|
|
4543
|
+
const requestStartRaw = Date.now();
|
|
4544
|
+
const headerTs = readHeaderNumber(req.headers[REQUEST_START_HEADER]) ?? queryTs;
|
|
4545
|
+
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw;
|
|
4546
|
+
let backendLiveState = null;
|
|
4547
|
+
let backendLiveChild = null;
|
|
4548
|
+
const headerSid = req.headers['x-bug-session-id'] || '';
|
|
4549
|
+
const headerAid = req.headers['x-bug-action-id'] || '';
|
|
4550
|
+
const hasIncomingReproSession = Boolean(headerSid || querySid || headerAid || queryAid);
|
|
4551
|
+
const backendLiveContext = !hasIncomingReproSession
|
|
4552
|
+
? await resolveBackendLiveContext(cfg, req, requestEpochMs)
|
|
4553
|
+
: null;
|
|
4554
|
+
const taggedActorIdentity = hasIncomingReproSession
|
|
4555
|
+
? await resolveLiveActorIdentity(cfg, req)
|
|
4556
|
+
: null;
|
|
4557
|
+
if (backendLiveContext) {
|
|
4558
|
+
backendLiveState = backendLiveActors.get(backendLiveContext.child.actorId) ?? null;
|
|
4559
|
+
backendLiveChild = backendLiveContext.child;
|
|
4560
|
+
}
|
|
4561
|
+
const sid = headerSid || querySid || backendLiveContext?.sid || '';
|
|
4562
|
+
const aid = headerAid || queryAid || backendLiveContext?.aid || '';
|
|
4170
4563
|
if (!sid || !aid) {
|
|
4171
4564
|
res.on('finish', endTrackedClientRequest);
|
|
4172
4565
|
res.on('close', endTrackedClientRequest);
|
|
4173
4566
|
return next(); // only capture tagged requests
|
|
4174
4567
|
}
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4568
|
+
const captureCfg = backendLiveChild
|
|
4569
|
+
? {
|
|
4570
|
+
...cfg,
|
|
4571
|
+
actorId: backendLiveChild.actorId,
|
|
4572
|
+
actorType: backendLiveChild.actorType,
|
|
4573
|
+
actorLabels: backendLiveChild.actorLabels,
|
|
4574
|
+
}
|
|
4575
|
+
: taggedActorIdentity
|
|
4576
|
+
? {
|
|
4577
|
+
...cfg,
|
|
4578
|
+
actorId: taggedActorIdentity.actorId,
|
|
4579
|
+
actorType: taggedActorIdentity.actorType,
|
|
4580
|
+
actorLabels: taggedActorIdentity.actorLabels,
|
|
4581
|
+
}
|
|
4582
|
+
: cfg;
|
|
4583
|
+
if (taggedActorIdentity) {
|
|
4584
|
+
post(captureCfg, sid, {
|
|
4585
|
+
entries: [{
|
|
4586
|
+
actionId: aid,
|
|
4587
|
+
sessionActor: {
|
|
4588
|
+
actorId: taggedActorIdentity.actorId,
|
|
4589
|
+
actorType: taggedActorIdentity.actorType,
|
|
4590
|
+
actorLabels: taggedActorIdentity.actorLabels,
|
|
4591
|
+
source: 'node-sdk',
|
|
4592
|
+
},
|
|
4593
|
+
t: requestEpochMs,
|
|
4594
|
+
}],
|
|
4595
|
+
}, { priority: 'high' });
|
|
4596
|
+
}
|
|
4597
|
+
const resolvePrivacy = () => getRuntimePrivacyState(captureCfg).policy ?? null;
|
|
4178
4598
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
4179
|
-
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
4180
4599
|
const rid = nextRequestId(requestEpochMs);
|
|
4181
4600
|
const captureScope = createRequestCaptureScope(rid, 'http');
|
|
4182
4601
|
const t0 = requestStartRaw;
|
|
@@ -4380,15 +4799,15 @@ function reproMiddleware(cfg) {
|
|
|
4380
4799
|
const activePrivacy = resolvePrivacy();
|
|
4381
4800
|
const requestBodyRaw = req.body;
|
|
4382
4801
|
const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
|
|
4383
|
-
?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw),
|
|
4802
|
+
?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4384
4803
|
const requestBody = requestBodyMaterialization.value;
|
|
4385
|
-
const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params),
|
|
4386
|
-
const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query),
|
|
4387
|
-
const maskedHeaders = await applyPrivacyThenMaskAsync('request.headers', requestHeaders,
|
|
4804
|
+
const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4805
|
+
const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4806
|
+
const maskedHeaders = await applyPrivacyThenMaskAsync('request.headers', requestHeaders, captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4388
4807
|
const responseBodyMaterialization = capturedBody === undefined
|
|
4389
4808
|
? { value: undefined }
|
|
4390
4809
|
: limitRawInlinePrivacyValue('response.body', capturedBody)
|
|
4391
|
-
?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody),
|
|
4810
|
+
?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4392
4811
|
const responseBody = responseBodyMaterialization.value;
|
|
4393
4812
|
const requestValueEntries = [];
|
|
4394
4813
|
const bodyValueCapture = requestBodyMaterialization.skipped
|
|
@@ -4398,7 +4817,7 @@ function reproMiddleware(cfg) {
|
|
|
4398
4817
|
rawValue: req.body,
|
|
4399
4818
|
previewValue: requestBody,
|
|
4400
4819
|
capture: {
|
|
4401
|
-
runtimeConfig:
|
|
4820
|
+
runtimeConfig: captureCfg,
|
|
4402
4821
|
captureHeaders: cfg.captureHeaders,
|
|
4403
4822
|
maskReq,
|
|
4404
4823
|
trace: endpointTraceCtx,
|
|
@@ -4411,7 +4830,7 @@ function reproMiddleware(cfg) {
|
|
|
4411
4830
|
rawValue: req.params,
|
|
4412
4831
|
previewValue: requestParams,
|
|
4413
4832
|
capture: {
|
|
4414
|
-
runtimeConfig:
|
|
4833
|
+
runtimeConfig: captureCfg,
|
|
4415
4834
|
captureHeaders: cfg.captureHeaders,
|
|
4416
4835
|
maskReq,
|
|
4417
4836
|
trace: endpointTraceCtx,
|
|
@@ -4424,7 +4843,7 @@ function reproMiddleware(cfg) {
|
|
|
4424
4843
|
rawValue: req.query,
|
|
4425
4844
|
previewValue: requestQuery,
|
|
4426
4845
|
capture: {
|
|
4427
|
-
runtimeConfig:
|
|
4846
|
+
runtimeConfig: captureCfg,
|
|
4428
4847
|
captureHeaders: cfg.captureHeaders,
|
|
4429
4848
|
maskReq,
|
|
4430
4849
|
trace: endpointTraceCtx,
|
|
@@ -4437,7 +4856,7 @@ function reproMiddleware(cfg) {
|
|
|
4437
4856
|
rawValue: req.headers,
|
|
4438
4857
|
previewValue: maskedHeaders,
|
|
4439
4858
|
capture: {
|
|
4440
|
-
runtimeConfig:
|
|
4859
|
+
runtimeConfig: captureCfg,
|
|
4441
4860
|
captureHeaders: cfg.captureHeaders,
|
|
4442
4861
|
maskReq,
|
|
4443
4862
|
trace: endpointTraceCtx,
|
|
@@ -4452,7 +4871,7 @@ function reproMiddleware(cfg) {
|
|
|
4452
4871
|
rawValue: capturedBody,
|
|
4453
4872
|
previewValue: responseBody,
|
|
4454
4873
|
capture: {
|
|
4455
|
-
runtimeConfig:
|
|
4874
|
+
runtimeConfig: captureCfg,
|
|
4456
4875
|
captureHeaders: cfg.captureHeaders,
|
|
4457
4876
|
maskReq,
|
|
4458
4877
|
trace: endpointTraceCtx,
|
|
@@ -4513,7 +4932,7 @@ function reproMiddleware(cfg) {
|
|
|
4513
4932
|
}
|
|
4514
4933
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
4515
4934
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
|
|
4516
|
-
post(
|
|
4935
|
+
post(captureCfg, sid, {
|
|
4517
4936
|
entries: [{
|
|
4518
4937
|
actionId: aid,
|
|
4519
4938
|
request: requestPayload,
|
|
@@ -4531,7 +4950,7 @@ function reproMiddleware(cfg) {
|
|
|
4531
4950
|
if (requestCaptureManifestScheduled)
|
|
4532
4951
|
return;
|
|
4533
4952
|
requestCaptureManifestScheduled = true;
|
|
4534
|
-
post(
|
|
4953
|
+
post(captureCfg, sid, {
|
|
4535
4954
|
entries: [{
|
|
4536
4955
|
actionId: aid,
|
|
4537
4956
|
requestRid: rid,
|
|
@@ -4634,7 +5053,7 @@ function reproMiddleware(cfg) {
|
|
|
4634
5053
|
try {
|
|
4635
5054
|
const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
|
|
4636
5055
|
const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
|
|
4637
|
-
cfg,
|
|
5056
|
+
cfg: captureCfg,
|
|
4638
5057
|
maskReq,
|
|
4639
5058
|
masking,
|
|
4640
5059
|
resolvePrivacy,
|
|
@@ -4662,7 +5081,7 @@ function reproMiddleware(cfg) {
|
|
|
4662
5081
|
for (let i = 0; i < traceBatches.length; i++) {
|
|
4663
5082
|
const batch = traceBatches[i];
|
|
4664
5083
|
const traceValues = collectBatchTraceValueEntries(batch, i);
|
|
4665
|
-
post(
|
|
5084
|
+
post(captureCfg, sid, {
|
|
4666
5085
|
entries: [{
|
|
4667
5086
|
actionId: aid,
|
|
4668
5087
|
trace: batch,
|
|
@@ -4684,6 +5103,9 @@ function reproMiddleware(cfg) {
|
|
|
4684
5103
|
}
|
|
4685
5104
|
sessionDrainWait = waitForSessionDrain(sid);
|
|
4686
5105
|
endSessionRequest(sid);
|
|
5106
|
+
if (backendLiveState && backendLiveChild) {
|
|
5107
|
+
releaseBackendLiveRequest(cfg, normalizeLiveModeConfig(cfg), backendLiveState, backendLiveChild, finishedAtAligned ?? alignedNow());
|
|
5108
|
+
}
|
|
4687
5109
|
endTrackedClientRequest();
|
|
4688
5110
|
if (__TRACER_READY) {
|
|
4689
5111
|
bumpIdle();
|
|
@@ -8801,9 +9223,11 @@ async function initRepro(cfg) {
|
|
|
8801
9223
|
const ingestCfg = toIngestConfig(cfg);
|
|
8802
9224
|
reproAutoMiddlewareConfig = {
|
|
8803
9225
|
...ingestCfg,
|
|
9226
|
+
apiBase: cfg.apiBase,
|
|
8804
9227
|
captureHeaders: cfg.captureHeaders,
|
|
8805
9228
|
masking: cfg.masking,
|
|
8806
9229
|
privacy: cfg.privacy,
|
|
9230
|
+
liveMode: cfg.liveMode,
|
|
8807
9231
|
};
|
|
8808
9232
|
const integrationCfg = {
|
|
8809
9233
|
...reproAutoMiddlewareConfig,
|
|
@@ -8855,6 +9279,9 @@ exports.__reproTestHooks = {
|
|
|
8855
9279
|
lastClientActivityAt = 0;
|
|
8856
9280
|
sdkBackgroundQuietUntil = 0;
|
|
8857
9281
|
},
|
|
9282
|
+
setBackendLiveTimingForTest(override) {
|
|
9283
|
+
backendLiveTimingOverrideForTest = override;
|
|
9284
|
+
},
|
|
8858
9285
|
runWithRequestContextForTest(ctx, fn) {
|
|
8859
9286
|
return als.run({
|
|
8860
9287
|
...ctx,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
13
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
|
-
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js",
|
|
15
|
+
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js",
|
|
16
16
|
"test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js && node test/mongodb-mongoose-native-suppression.testcontainers.test.js"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|