@reproapp/node-sdk 0.0.37 → 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 +488 -22
- package/package.json +2 -2
- package/src/index.ts +721 -22
- package/test/backend-live-mode.test.js +403 -0
- package/test/mongodb-integration.test.js +164 -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;
|
|
@@ -4092,6 +4401,111 @@ async function maybeCaptureDbValueAsync(params, sink) {
|
|
|
4092
4401
|
}
|
|
4093
4402
|
return result.captureRef;
|
|
4094
4403
|
}
|
|
4404
|
+
function pushUniqueCaptureCandidate(candidates, value) {
|
|
4405
|
+
if (value === undefined)
|
|
4406
|
+
return;
|
|
4407
|
+
if (candidates.some((candidate) => candidate === value))
|
|
4408
|
+
return;
|
|
4409
|
+
candidates.push(value);
|
|
4410
|
+
}
|
|
4411
|
+
function buildResultMetaFullValueCandidates(resultMetaRaw, resultMetaSource) {
|
|
4412
|
+
const candidates = [];
|
|
4413
|
+
pushUniqueCaptureCandidate(candidates, resultMetaRaw);
|
|
4414
|
+
if (resultMetaSource &&
|
|
4415
|
+
typeof resultMetaSource === 'object' &&
|
|
4416
|
+
Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')) {
|
|
4417
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
|
|
4418
|
+
}
|
|
4419
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource);
|
|
4420
|
+
return candidates;
|
|
4421
|
+
}
|
|
4422
|
+
async function maybeCaptureDbValueFromCandidatesAsync(params, sink) {
|
|
4423
|
+
let skippedRef;
|
|
4424
|
+
for (const rawValue of params.rawValues) {
|
|
4425
|
+
const result = await createCapturedValueEntryAsync({
|
|
4426
|
+
target: params.target,
|
|
4427
|
+
rawValue,
|
|
4428
|
+
previewValue: params.previewValue,
|
|
4429
|
+
capture: params.capture,
|
|
4430
|
+
});
|
|
4431
|
+
if (!result)
|
|
4432
|
+
continue;
|
|
4433
|
+
if (result.entry) {
|
|
4434
|
+
sink.push(result.entry);
|
|
4435
|
+
return result.captureRef;
|
|
4436
|
+
}
|
|
4437
|
+
if (!skippedRef) {
|
|
4438
|
+
skippedRef = result.captureRef;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
return skippedRef;
|
|
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();
|
|
4095
4509
|
function reproMiddleware(cfg) {
|
|
4096
4510
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4097
4511
|
patchMongoDb(cfg);
|
|
@@ -4126,18 +4540,62 @@ function reproMiddleware(cfg) {
|
|
|
4126
4540
|
clientRequestEnded = true;
|
|
4127
4541
|
endClientRequest();
|
|
4128
4542
|
};
|
|
4129
|
-
const
|
|
4130
|
-
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 || '';
|
|
4131
4563
|
if (!sid || !aid) {
|
|
4132
4564
|
res.on('finish', endTrackedClientRequest);
|
|
4133
4565
|
res.on('close', endTrackedClientRequest);
|
|
4134
4566
|
return next(); // only capture tagged requests
|
|
4135
4567
|
}
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4138
|
-
|
|
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;
|
|
4139
4598
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
4140
|
-
const requestEpochMs = headerTs !== null ? headerTs : requestStartRaw + clockSkewMs;
|
|
4141
4599
|
const rid = nextRequestId(requestEpochMs);
|
|
4142
4600
|
const captureScope = createRequestCaptureScope(rid, 'http');
|
|
4143
4601
|
const t0 = requestStartRaw;
|
|
@@ -4341,15 +4799,15 @@ function reproMiddleware(cfg) {
|
|
|
4341
4799
|
const activePrivacy = resolvePrivacy();
|
|
4342
4800
|
const requestBodyRaw = req.body;
|
|
4343
4801
|
const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
|
|
4344
|
-
?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw),
|
|
4802
|
+
?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4345
4803
|
const requestBody = requestBodyMaterialization.value;
|
|
4346
|
-
const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params),
|
|
4347
|
-
const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query),
|
|
4348
|
-
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);
|
|
4349
4807
|
const responseBodyMaterialization = capturedBody === undefined
|
|
4350
4808
|
? { value: undefined }
|
|
4351
4809
|
: limitRawInlinePrivacyValue('response.body', capturedBody)
|
|
4352
|
-
?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody),
|
|
4810
|
+
?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
|
|
4353
4811
|
const responseBody = responseBodyMaterialization.value;
|
|
4354
4812
|
const requestValueEntries = [];
|
|
4355
4813
|
const bodyValueCapture = requestBodyMaterialization.skipped
|
|
@@ -4359,7 +4817,7 @@ function reproMiddleware(cfg) {
|
|
|
4359
4817
|
rawValue: req.body,
|
|
4360
4818
|
previewValue: requestBody,
|
|
4361
4819
|
capture: {
|
|
4362
|
-
runtimeConfig:
|
|
4820
|
+
runtimeConfig: captureCfg,
|
|
4363
4821
|
captureHeaders: cfg.captureHeaders,
|
|
4364
4822
|
maskReq,
|
|
4365
4823
|
trace: endpointTraceCtx,
|
|
@@ -4372,7 +4830,7 @@ function reproMiddleware(cfg) {
|
|
|
4372
4830
|
rawValue: req.params,
|
|
4373
4831
|
previewValue: requestParams,
|
|
4374
4832
|
capture: {
|
|
4375
|
-
runtimeConfig:
|
|
4833
|
+
runtimeConfig: captureCfg,
|
|
4376
4834
|
captureHeaders: cfg.captureHeaders,
|
|
4377
4835
|
maskReq,
|
|
4378
4836
|
trace: endpointTraceCtx,
|
|
@@ -4385,7 +4843,7 @@ function reproMiddleware(cfg) {
|
|
|
4385
4843
|
rawValue: req.query,
|
|
4386
4844
|
previewValue: requestQuery,
|
|
4387
4845
|
capture: {
|
|
4388
|
-
runtimeConfig:
|
|
4846
|
+
runtimeConfig: captureCfg,
|
|
4389
4847
|
captureHeaders: cfg.captureHeaders,
|
|
4390
4848
|
maskReq,
|
|
4391
4849
|
trace: endpointTraceCtx,
|
|
@@ -4398,7 +4856,7 @@ function reproMiddleware(cfg) {
|
|
|
4398
4856
|
rawValue: req.headers,
|
|
4399
4857
|
previewValue: maskedHeaders,
|
|
4400
4858
|
capture: {
|
|
4401
|
-
runtimeConfig:
|
|
4859
|
+
runtimeConfig: captureCfg,
|
|
4402
4860
|
captureHeaders: cfg.captureHeaders,
|
|
4403
4861
|
maskReq,
|
|
4404
4862
|
trace: endpointTraceCtx,
|
|
@@ -4413,7 +4871,7 @@ function reproMiddleware(cfg) {
|
|
|
4413
4871
|
rawValue: capturedBody,
|
|
4414
4872
|
previewValue: responseBody,
|
|
4415
4873
|
capture: {
|
|
4416
|
-
runtimeConfig:
|
|
4874
|
+
runtimeConfig: captureCfg,
|
|
4417
4875
|
captureHeaders: cfg.captureHeaders,
|
|
4418
4876
|
maskReq,
|
|
4419
4877
|
trace: endpointTraceCtx,
|
|
@@ -4474,7 +4932,7 @@ function reproMiddleware(cfg) {
|
|
|
4474
4932
|
}
|
|
4475
4933
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
4476
4934
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
|
|
4477
|
-
post(
|
|
4935
|
+
post(captureCfg, sid, {
|
|
4478
4936
|
entries: [{
|
|
4479
4937
|
actionId: aid,
|
|
4480
4938
|
request: requestPayload,
|
|
@@ -4492,7 +4950,7 @@ function reproMiddleware(cfg) {
|
|
|
4492
4950
|
if (requestCaptureManifestScheduled)
|
|
4493
4951
|
return;
|
|
4494
4952
|
requestCaptureManifestScheduled = true;
|
|
4495
|
-
post(
|
|
4953
|
+
post(captureCfg, sid, {
|
|
4496
4954
|
entries: [{
|
|
4497
4955
|
actionId: aid,
|
|
4498
4956
|
requestRid: rid,
|
|
@@ -4595,7 +5053,7 @@ function reproMiddleware(cfg) {
|
|
|
4595
5053
|
try {
|
|
4596
5054
|
const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
|
|
4597
5055
|
const materializedEvents = await materializePendingTraceEventsAsync(pendingEvents, {
|
|
4598
|
-
cfg,
|
|
5056
|
+
cfg: captureCfg,
|
|
4599
5057
|
maskReq,
|
|
4600
5058
|
masking,
|
|
4601
5059
|
resolvePrivacy,
|
|
@@ -4623,7 +5081,7 @@ function reproMiddleware(cfg) {
|
|
|
4623
5081
|
for (let i = 0; i < traceBatches.length; i++) {
|
|
4624
5082
|
const batch = traceBatches[i];
|
|
4625
5083
|
const traceValues = collectBatchTraceValueEntries(batch, i);
|
|
4626
|
-
post(
|
|
5084
|
+
post(captureCfg, sid, {
|
|
4627
5085
|
entries: [{
|
|
4628
5086
|
actionId: aid,
|
|
4629
5087
|
trace: batch,
|
|
@@ -4645,6 +5103,9 @@ function reproMiddleware(cfg) {
|
|
|
4645
5103
|
}
|
|
4646
5104
|
sessionDrainWait = waitForSessionDrain(sid);
|
|
4647
5105
|
endSessionRequest(sid);
|
|
5106
|
+
if (backendLiveState && backendLiveChild) {
|
|
5107
|
+
releaseBackendLiveRequest(cfg, normalizeLiveModeConfig(cfg), backendLiveState, backendLiveChild, finishedAtAligned ?? alignedNow());
|
|
5108
|
+
}
|
|
4648
5109
|
endTrackedClientRequest();
|
|
4649
5110
|
if (__TRACER_READY) {
|
|
4650
5111
|
bumpIdle();
|
|
@@ -5285,9 +5746,9 @@ async function emitDbQueryAsync(cfg, sid, aid, payload) {
|
|
|
5285
5746
|
previewValue: queryPreview,
|
|
5286
5747
|
capture,
|
|
5287
5748
|
}, dbValues);
|
|
5288
|
-
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await
|
|
5749
|
+
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
|
|
5289
5750
|
target: 'db.resultMeta',
|
|
5290
|
-
|
|
5751
|
+
rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
|
|
5291
5752
|
previewValue: resultMetaPreview,
|
|
5292
5753
|
capture,
|
|
5293
5754
|
}, dbValues);
|
|
@@ -8762,9 +9223,11 @@ async function initRepro(cfg) {
|
|
|
8762
9223
|
const ingestCfg = toIngestConfig(cfg);
|
|
8763
9224
|
reproAutoMiddlewareConfig = {
|
|
8764
9225
|
...ingestCfg,
|
|
9226
|
+
apiBase: cfg.apiBase,
|
|
8765
9227
|
captureHeaders: cfg.captureHeaders,
|
|
8766
9228
|
masking: cfg.masking,
|
|
8767
9229
|
privacy: cfg.privacy,
|
|
9230
|
+
liveMode: cfg.liveMode,
|
|
8768
9231
|
};
|
|
8769
9232
|
const integrationCfg = {
|
|
8770
9233
|
...reproAutoMiddlewareConfig,
|
|
@@ -8816,6 +9279,9 @@ exports.__reproTestHooks = {
|
|
|
8816
9279
|
lastClientActivityAt = 0;
|
|
8817
9280
|
sdkBackgroundQuietUntil = 0;
|
|
8818
9281
|
},
|
|
9282
|
+
setBackendLiveTimingForTest(override) {
|
|
9283
|
+
backendLiveTimingOverrideForTest = override;
|
|
9284
|
+
},
|
|
8819
9285
|
runWithRequestContextForTest(ctx, fn) {
|
|
8820
9286
|
return als.run({
|
|
8821
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": {
|