@henrique-olivier/network-listener 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 ADDED
@@ -0,0 +1,122 @@
1
+ # Lib Escutadora de Rede
2
+
3
+ POC de uma biblioteca leve e agnostica de framework para observar requisicoes HTTP no front-end e produzir diagnosticos provaveis sobre lentidao, erro ou instabilidade.
4
+
5
+ ## Objetivos
6
+
7
+ - observar requisicoes feitas via `fetch`;
8
+ - observar requisicoes feitas via Axios;
9
+ - manter uma janela das ultimas requisicoes em memoria;
10
+ - normalizar rotas para reduzir exposicao de dados sensiveis;
11
+ - calcular metricas simples;
12
+ - classificar o estado atual da comunicacao;
13
+ - expor uma API simples para qualquer front-end, inclusive projetos legados.
14
+
15
+ ## Compatibilidade
16
+
17
+ O build da POC foi configurado para:
18
+
19
+ - CommonJS;
20
+ - JavaScript ES5;
21
+ - declaracoes TypeScript;
22
+ - sem dependencia de React;
23
+ - sem dependencia obrigatoria de Axios;
24
+ - sem polyfills globais;
25
+ - sem leitura de body de request ou response.
26
+
27
+ ## Uso
28
+
29
+ ```ts
30
+ import { createNetworkListener } from '@henrique-olivier/network-listener';
31
+
32
+ const listener = createNetworkListener({
33
+ slowRequestThresholdMs: 1500,
34
+ maxSamples: 20,
35
+ minimumSamplesToDiagnose: 5,
36
+ });
37
+
38
+ listener.installFetch();
39
+ listener.installAxios(axiosInstance);
40
+
41
+ const unsubscribe = listener.subscribe(function (diagnosis) {
42
+ console.log(diagnosis);
43
+ });
44
+
45
+ listener.start();
46
+
47
+ // Depois, quando nao precisar mais observar:
48
+ listener.stop();
49
+ unsubscribe();
50
+ ```
51
+
52
+ ## Diagnostico
53
+
54
+ O snapshot atual pode ser obtido a qualquer momento:
55
+
56
+ ```ts
57
+ const diagnosis = listener.getSnapshot();
58
+ ```
59
+
60
+ Exemplo de retorno:
61
+
62
+ ```ts
63
+ {
64
+ status: 'degraded',
65
+ probableCause: 'specific-endpoint',
66
+ confidenceLevel: 'medium',
67
+ reasons: ['Only one endpoint concentrates slow requests.'],
68
+ summary: {
69
+ requestCount: 20,
70
+ errorRate: 0,
71
+ timeoutRate: 0,
72
+ slowRequestRate: 0.35,
73
+ medianDurationMs: 700,
74
+ p95DurationMs: 2300,
75
+ affectedEndpointRatio: 0.2
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Privacidade
81
+
82
+ A lib nao coleta por padrao:
83
+
84
+ - headers;
85
+ - cookies;
86
+ - payload;
87
+ - corpo de resposta;
88
+ - query strings completas;
89
+ - geolocalizacao;
90
+ - IP.
91
+
92
+ O principal identificador tecnico e `normalizedRoute`.
93
+
94
+ Exemplo:
95
+
96
+ ```txt
97
+ /api/users/123/orders?token=abc
98
+ ```
99
+
100
+ vira:
101
+
102
+ ```txt
103
+ /api/users/:id/orders
104
+ ```
105
+
106
+ ## Scripts
107
+
108
+ ```bash
109
+ npm test
110
+ npm run build
111
+ ```
112
+
113
+ ## Escopo fora da POC
114
+
115
+ - XHR;
116
+ - health-check automatico;
117
+ - teste de download;
118
+ - PerformanceObserver;
119
+ - Server-Timing;
120
+ - navigator.connection;
121
+ - dashboard;
122
+ - envio de telemetria para backend.
@@ -0,0 +1,11 @@
1
+ import { AxiosLike, NetworkRequestEvent, RouteNormalizer, Uninstall } from '../core/types';
2
+ export type AxiosAdapterOptions = {
3
+ axiosInstance: AxiosLike;
4
+ isStarted: () => boolean;
5
+ onEvent: (event: NetworkRequestEvent) => void;
6
+ routeNormalizer: RouteNormalizer;
7
+ timeoutThresholdMs: number;
8
+ now: () => number;
9
+ nextRequestId: () => string;
10
+ };
11
+ export declare function installAxiosAdapter(options: AxiosAdapterOptions): Uninstall;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installAxiosAdapter = void 0;
4
+ var META_KEY = '__networkListenerMeta';
5
+ function buildAxiosUrl(config) {
6
+ var baseURL = config && config.baseURL ? String(config.baseURL) : '';
7
+ var url = config && config.url ? String(config.url) : '';
8
+ if (!baseURL || url.indexOf('http://') === 0 || url.indexOf('https://') === 0 || url.indexOf('//') === 0) {
9
+ return url;
10
+ }
11
+ if (baseURL.charAt(baseURL.length - 1) === '/' && url.charAt(0) === '/') {
12
+ return baseURL + url.substring(1);
13
+ }
14
+ if (baseURL.charAt(baseURL.length - 1) !== '/' && url.charAt(0) !== '/') {
15
+ return baseURL + '/' + url;
16
+ }
17
+ return baseURL + url;
18
+ }
19
+ function getAxiosMethod(config) {
20
+ return config && config.method ? String(config.method).toUpperCase() : 'GET';
21
+ }
22
+ function classifyAxiosError(error, durationMs, timeoutThresholdMs) {
23
+ var code = error && error.code ? String(error.code) : '';
24
+ var message = error && error.message ? String(error.message) : '';
25
+ var status = error && error.response && typeof error.response.status === 'number' ? error.response.status : undefined;
26
+ var timedOut = code === 'ECONNABORTED' || durationMs >= timeoutThresholdMs || /timeout/i.test(message);
27
+ if (timedOut) {
28
+ return { errorType: 'timeout', timedOut: true, aborted: false };
29
+ }
30
+ if (status && status >= 500) {
31
+ return { errorType: 'http-server', timedOut: false, aborted: false };
32
+ }
33
+ if (status && status >= 400) {
34
+ return { errorType: 'http-client', timedOut: false, aborted: false };
35
+ }
36
+ if (code === 'ERR_CANCELED') {
37
+ return { errorType: 'aborted', timedOut: false, aborted: true };
38
+ }
39
+ return { errorType: 'network', timedOut: false, aborted: false };
40
+ }
41
+ function installAxiosAdapter(options) {
42
+ var axiosInstance = options.axiosInstance;
43
+ var requestInterceptorId;
44
+ var responseInterceptorId;
45
+ requestInterceptorId = axiosInstance.interceptors.request.use(function onRequest(config) {
46
+ var url = buildAxiosUrl(config || {});
47
+ var route = options.routeNormalizer(url);
48
+ config = config || {};
49
+ config[META_KEY] = {
50
+ requestId: options.nextRequestId(),
51
+ startedAt: options.now(),
52
+ method: getAxiosMethod(config),
53
+ url: url,
54
+ route: route,
55
+ };
56
+ return config;
57
+ });
58
+ responseInterceptorId = axiosInstance.interceptors.response.use(function onResponse(response) {
59
+ var config = response && response.config ? response.config : {};
60
+ var meta = config[META_KEY];
61
+ var finishedAt;
62
+ var status;
63
+ var durationMs;
64
+ var success;
65
+ if (meta && options.isStarted()) {
66
+ finishedAt = options.now();
67
+ status = response && typeof response.status === 'number' ? response.status : undefined;
68
+ durationMs = finishedAt - meta.startedAt;
69
+ success = typeof status === 'number' ? status < 400 : true;
70
+ options.onEvent({
71
+ requestId: meta.requestId,
72
+ source: 'axios',
73
+ method: meta.method,
74
+ url: meta.url,
75
+ route: meta.route,
76
+ normalizedRoute: meta.route,
77
+ startedAt: meta.startedAt,
78
+ finishedAt: finishedAt,
79
+ durationMs: durationMs,
80
+ status: status,
81
+ success: success,
82
+ errorType: success ? undefined : status && status >= 500 ? 'http-server' : 'http-client',
83
+ timedOut: durationMs >= options.timeoutThresholdMs,
84
+ aborted: false,
85
+ });
86
+ }
87
+ return response;
88
+ }, function onError(error) {
89
+ var config = error && error.config ? error.config : {};
90
+ var meta = config[META_KEY];
91
+ var finishedAt;
92
+ var durationMs;
93
+ var errorInfo;
94
+ var status;
95
+ if (meta && options.isStarted()) {
96
+ finishedAt = options.now();
97
+ durationMs = finishedAt - meta.startedAt;
98
+ errorInfo = classifyAxiosError(error, durationMs, options.timeoutThresholdMs);
99
+ status = error && error.response && typeof error.response.status === 'number' ? error.response.status : undefined;
100
+ options.onEvent({
101
+ requestId: meta.requestId,
102
+ source: 'axios',
103
+ method: meta.method,
104
+ url: meta.url,
105
+ route: meta.route,
106
+ normalizedRoute: meta.route,
107
+ startedAt: meta.startedAt,
108
+ finishedAt: finishedAt,
109
+ durationMs: durationMs,
110
+ status: status,
111
+ success: false,
112
+ errorType: errorInfo.errorType,
113
+ timedOut: errorInfo.timedOut,
114
+ aborted: errorInfo.aborted,
115
+ });
116
+ }
117
+ throw error;
118
+ });
119
+ return function uninstallAxiosAdapter() {
120
+ axiosInstance.interceptors.request.eject(requestInterceptorId);
121
+ axiosInstance.interceptors.response.eject(responseInterceptorId);
122
+ };
123
+ }
124
+ exports.installAxiosAdapter = installAxiosAdapter;
@@ -0,0 +1,11 @@
1
+ import { FetchTarget, NetworkRequestEvent, RouteNormalizer, Uninstall } from '../core/types';
2
+ export type FetchAdapterOptions = {
3
+ target?: FetchTarget;
4
+ isStarted: () => boolean;
5
+ onEvent: (event: NetworkRequestEvent) => void;
6
+ routeNormalizer: RouteNormalizer;
7
+ timeoutThresholdMs: number;
8
+ now: () => number;
9
+ nextRequestId: () => string;
10
+ };
11
+ export declare function installFetchAdapter(options: FetchAdapterOptions): Uninstall;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installFetchAdapter = void 0;
4
+ var MARKER = '__networkListenerFetchInstalled';
5
+ var ORIGINAL = '__networkListenerOriginalFetch';
6
+ function getDefaultTarget() {
7
+ if (typeof window !== 'undefined') {
8
+ return window;
9
+ }
10
+ return undefined;
11
+ }
12
+ function getFetchUrl(input) {
13
+ if (typeof input === 'string') {
14
+ return input;
15
+ }
16
+ if (input && typeof input.url === 'string') {
17
+ return input.url;
18
+ }
19
+ return '';
20
+ }
21
+ function getFetchMethod(input, init) {
22
+ if (init && init.method) {
23
+ return String(init.method).toUpperCase();
24
+ }
25
+ if (input && input.method) {
26
+ return String(input.method).toUpperCase();
27
+ }
28
+ return 'GET';
29
+ }
30
+ function classifyError(error, durationMs, timeoutThresholdMs) {
31
+ var name = error && error.name ? String(error.name) : '';
32
+ var message = error && error.message ? String(error.message) : '';
33
+ var aborted = name === 'AbortError';
34
+ var timedOut = durationMs >= timeoutThresholdMs || /timeout/i.test(message);
35
+ return {
36
+ errorType: timedOut ? 'timeout' : aborted ? 'aborted' : 'network',
37
+ timedOut: timedOut,
38
+ aborted: aborted,
39
+ };
40
+ }
41
+ function installFetchAdapter(options) {
42
+ var target = options.target || getDefaultTarget();
43
+ var currentFetch;
44
+ var originalFetch;
45
+ if (!target || !target.fetch) {
46
+ return function noop() { };
47
+ }
48
+ currentFetch = target.fetch;
49
+ if (currentFetch[MARKER]) {
50
+ return function noop() { };
51
+ }
52
+ originalFetch = currentFetch;
53
+ function wrappedFetch() {
54
+ var args = arguments;
55
+ var input = args[0];
56
+ var init = args[1];
57
+ var url = getFetchUrl(input);
58
+ var route = options.routeNormalizer(url);
59
+ var method = getFetchMethod(input, init);
60
+ var requestId = options.nextRequestId();
61
+ var startedAt = options.now();
62
+ return originalFetch.apply(this, args).then(function onResponse(response) {
63
+ var finishedAt = options.now();
64
+ var status = response && typeof response.status === 'number' ? response.status : undefined;
65
+ var success = typeof status === 'number' ? status < 400 : true;
66
+ var durationMs = finishedAt - startedAt;
67
+ if (options.isStarted()) {
68
+ options.onEvent({
69
+ requestId: requestId,
70
+ source: 'fetch',
71
+ method: method,
72
+ url: url,
73
+ route: route,
74
+ normalizedRoute: route,
75
+ startedAt: startedAt,
76
+ finishedAt: finishedAt,
77
+ durationMs: durationMs,
78
+ status: status,
79
+ success: success,
80
+ errorType: success ? undefined : status && status >= 500 ? 'http-server' : 'http-client',
81
+ timedOut: durationMs >= options.timeoutThresholdMs,
82
+ aborted: false,
83
+ });
84
+ }
85
+ return response;
86
+ }, function onError(error) {
87
+ var finishedAt = options.now();
88
+ var durationMs = finishedAt - startedAt;
89
+ var errorInfo = classifyError(error, durationMs, options.timeoutThresholdMs);
90
+ if (options.isStarted()) {
91
+ options.onEvent({
92
+ requestId: requestId,
93
+ source: 'fetch',
94
+ method: method,
95
+ url: url,
96
+ route: route,
97
+ normalizedRoute: route,
98
+ startedAt: startedAt,
99
+ finishedAt: finishedAt,
100
+ durationMs: durationMs,
101
+ success: false,
102
+ errorType: errorInfo.errorType,
103
+ timedOut: errorInfo.timedOut,
104
+ aborted: errorInfo.aborted,
105
+ });
106
+ }
107
+ throw error;
108
+ });
109
+ }
110
+ wrappedFetch[MARKER] = true;
111
+ wrappedFetch[ORIGINAL] = originalFetch;
112
+ target.fetch = wrappedFetch;
113
+ return function uninstallFetchAdapter() {
114
+ if (target && target.fetch === wrappedFetch) {
115
+ target.fetch = originalFetch;
116
+ }
117
+ };
118
+ }
119
+ exports.installFetchAdapter = installFetchAdapter;
@@ -0,0 +1,2 @@
1
+ import { NetworkListener, NetworkListenerOptions } from './types';
2
+ export declare function createNetworkListener(options?: NetworkListenerOptions): NetworkListener;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNetworkListener = void 0;
4
+ var axiosAdapter_1 = require("../adapters/axiosAdapter");
5
+ var fetchAdapter_1 = require("../adapters/fetchAdapter");
6
+ var metricsStore_1 = require("./metricsStore");
7
+ var routeNormalizer_1 = require("./routeNormalizer");
8
+ var ruleEngine_1 = require("./ruleEngine");
9
+ var DEFAULT_SLOW_REQUEST_THRESHOLD_MS = 1500;
10
+ var DEFAULT_MAX_SAMPLES = 20;
11
+ var DEFAULT_MINIMUM_SAMPLES_TO_DIAGNOSE = 5;
12
+ var DEFAULT_TIMEOUT_THRESHOLD_MS = 30000;
13
+ function now() {
14
+ return new Date().getTime();
15
+ }
16
+ function createNetworkListener(options) {
17
+ var resolvedOptions = options || {};
18
+ var slowRequestThresholdMs = resolvedOptions.slowRequestThresholdMs || DEFAULT_SLOW_REQUEST_THRESHOLD_MS;
19
+ var maxSamples = resolvedOptions.maxSamples || DEFAULT_MAX_SAMPLES;
20
+ var minimumSamplesToDiagnose = resolvedOptions.minimumSamplesToDiagnose || DEFAULT_MINIMUM_SAMPLES_TO_DIAGNOSE;
21
+ var timeoutThresholdMs = resolvedOptions.timeoutThresholdMs || DEFAULT_TIMEOUT_THRESHOLD_MS;
22
+ var routeNormalizer = resolvedOptions.routeNormalizer || routeNormalizer_1.defaultRouteNormalizer;
23
+ var store = (0, metricsStore_1.createMetricsStore)({
24
+ maxSamples: maxSamples,
25
+ slowRequestThresholdMs: slowRequestThresholdMs,
26
+ });
27
+ var subscribers = [];
28
+ var started = false;
29
+ var requestSequence = 0;
30
+ var fetchUninstall;
31
+ var axiosUninstalls = [];
32
+ function nextRequestId() {
33
+ requestSequence += 1;
34
+ return 'nl_' + now() + '_' + requestSequence;
35
+ }
36
+ function getSnapshot() {
37
+ return (0, ruleEngine_1.diagnoseNetwork)(store.getEvents(), {
38
+ minimumSamplesToDiagnose: minimumSamplesToDiagnose,
39
+ slowRequestThresholdMs: slowRequestThresholdMs,
40
+ });
41
+ }
42
+ function notify() {
43
+ var snapshot = getSnapshot();
44
+ var copy = subscribers.slice();
45
+ var i;
46
+ for (i = 0; i < copy.length; i += 1) {
47
+ copy[i](snapshot);
48
+ }
49
+ }
50
+ function record(event) {
51
+ store.add(event);
52
+ notify();
53
+ }
54
+ return {
55
+ start: function start() {
56
+ started = true;
57
+ },
58
+ stop: function stop() {
59
+ started = false;
60
+ },
61
+ subscribe: function subscribe(subscriber) {
62
+ subscribers.push(subscriber);
63
+ return function unsubscribe() {
64
+ var index = subscribers.indexOf(subscriber);
65
+ if (index >= 0) {
66
+ subscribers.splice(index, 1);
67
+ }
68
+ };
69
+ },
70
+ getSnapshot: getSnapshot,
71
+ record: record,
72
+ installFetch: function installFetch(target) {
73
+ if (fetchUninstall) {
74
+ return fetchUninstall;
75
+ }
76
+ fetchUninstall = (0, fetchAdapter_1.installFetchAdapter)({
77
+ target: target,
78
+ isStarted: function isStarted() {
79
+ return started;
80
+ },
81
+ onEvent: record,
82
+ routeNormalizer: routeNormalizer,
83
+ timeoutThresholdMs: timeoutThresholdMs,
84
+ now: now,
85
+ nextRequestId: nextRequestId,
86
+ });
87
+ return fetchUninstall;
88
+ },
89
+ installAxios: function installAxios(axiosInstance) {
90
+ var uninstall = (0, axiosAdapter_1.installAxiosAdapter)({
91
+ axiosInstance: axiosInstance,
92
+ isStarted: function isStarted() {
93
+ return started;
94
+ },
95
+ onEvent: record,
96
+ routeNormalizer: routeNormalizer,
97
+ timeoutThresholdMs: timeoutThresholdMs,
98
+ now: now,
99
+ nextRequestId: nextRequestId,
100
+ });
101
+ axiosUninstalls.push(uninstall);
102
+ return function uninstallAxios() {
103
+ var index = axiosUninstalls.indexOf(uninstall);
104
+ if (index >= 0) {
105
+ axiosUninstalls.splice(index, 1);
106
+ }
107
+ uninstall();
108
+ };
109
+ },
110
+ };
111
+ }
112
+ exports.createNetworkListener = createNetworkListener;
@@ -0,0 +1,13 @@
1
+ import { NetworkRequestEvent, NetworkSummary } from './types';
2
+ export type MetricsStore = {
3
+ add: (event: NetworkRequestEvent) => NetworkSummary;
4
+ getEvents: () => NetworkRequestEvent[];
5
+ getSummary: () => NetworkSummary;
6
+ clear: () => void;
7
+ };
8
+ export type MetricsStoreOptions = {
9
+ maxSamples: number;
10
+ slowRequestThresholdMs: number;
11
+ };
12
+ export declare function calculateSummary(events: NetworkRequestEvent[], slowRequestThresholdMs: number): NetworkSummary;
13
+ export declare function createMetricsStore(options: MetricsStoreOptions): MetricsStore;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMetricsStore = exports.calculateSummary = void 0;
4
+ function createEmptySummary() {
5
+ return {
6
+ requestCount: 0,
7
+ errorRate: 0,
8
+ timeoutRate: 0,
9
+ slowRequestRate: 0,
10
+ medianDurationMs: 0,
11
+ p95DurationMs: 0,
12
+ affectedEndpointRatio: 0,
13
+ };
14
+ }
15
+ function percentile(sortedValues, percentileValue) {
16
+ var index;
17
+ if (sortedValues.length === 0) {
18
+ return 0;
19
+ }
20
+ index = Math.ceil((percentileValue / 100) * sortedValues.length) - 1;
21
+ if (index < 0) {
22
+ index = 0;
23
+ }
24
+ return sortedValues[index];
25
+ }
26
+ function calculateSummary(events, slowRequestThresholdMs) {
27
+ var total = events.length;
28
+ var errors = 0;
29
+ var timeouts = 0;
30
+ var slow = 0;
31
+ var durations = [];
32
+ var endpoints = {};
33
+ var affectedEndpoints = {};
34
+ var endpointCount = 0;
35
+ var affectedEndpointCount = 0;
36
+ var i;
37
+ var event;
38
+ var route;
39
+ var affected;
40
+ if (total === 0) {
41
+ return createEmptySummary();
42
+ }
43
+ for (i = 0; i < total; i += 1) {
44
+ event = events[i];
45
+ route = event.normalizedRoute;
46
+ affected = false;
47
+ durations.push(event.durationMs);
48
+ if (!event.success) {
49
+ errors += 1;
50
+ affected = true;
51
+ }
52
+ if (event.timedOut) {
53
+ timeouts += 1;
54
+ affected = true;
55
+ }
56
+ if (event.durationMs >= slowRequestThresholdMs) {
57
+ slow += 1;
58
+ affected = true;
59
+ }
60
+ if (!endpoints[route]) {
61
+ endpoints[route] = true;
62
+ endpointCount += 1;
63
+ }
64
+ if (affected && !affectedEndpoints[route]) {
65
+ affectedEndpoints[route] = true;
66
+ affectedEndpointCount += 1;
67
+ }
68
+ }
69
+ durations.sort(function (a, b) {
70
+ return a - b;
71
+ });
72
+ return {
73
+ requestCount: total,
74
+ errorRate: errors / total,
75
+ timeoutRate: timeouts / total,
76
+ slowRequestRate: slow / total,
77
+ medianDurationMs: percentile(durations, 50),
78
+ p95DurationMs: percentile(durations, 95),
79
+ affectedEndpointRatio: endpointCount === 0 ? 0 : affectedEndpointCount / endpointCount,
80
+ };
81
+ }
82
+ exports.calculateSummary = calculateSummary;
83
+ function createMetricsStore(options) {
84
+ var events = [];
85
+ return {
86
+ add: function add(event) {
87
+ events.push(event);
88
+ while (events.length > options.maxSamples) {
89
+ events.shift();
90
+ }
91
+ return calculateSummary(events, options.slowRequestThresholdMs);
92
+ },
93
+ getEvents: function getEvents() {
94
+ return events.slice();
95
+ },
96
+ getSummary: function getSummary() {
97
+ return calculateSummary(events, options.slowRequestThresholdMs);
98
+ },
99
+ clear: function clear() {
100
+ events = [];
101
+ },
102
+ };
103
+ }
104
+ exports.createMetricsStore = createMetricsStore;
@@ -0,0 +1,2 @@
1
+ import { RouteNormalizer } from './types';
2
+ export declare const defaultRouteNormalizer: RouteNormalizer;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultRouteNormalizer = void 0;
4
+ function stripQueryAndHash(value) {
5
+ var withoutHash = value.split('#')[0];
6
+ return withoutHash.split('?')[0];
7
+ }
8
+ function extractPath(value) {
9
+ var clean = stripQueryAndHash(value || '');
10
+ var protocolIndex = clean.indexOf('://');
11
+ var pathStart;
12
+ if (protocolIndex >= 0) {
13
+ pathStart = clean.indexOf('/', protocolIndex + 3);
14
+ return pathStart >= 0 ? clean.substring(pathStart) : '/';
15
+ }
16
+ if (clean.indexOf('//') === 0) {
17
+ pathStart = clean.indexOf('/', 2);
18
+ return pathStart >= 0 ? clean.substring(pathStart) : '/';
19
+ }
20
+ return clean || '/';
21
+ }
22
+ function looksLikeId(segment) {
23
+ if (/^\d+$/.test(segment)) {
24
+ return true;
25
+ }
26
+ if (/^[0-9a-fA-F]{8,}$/.test(segment)) {
27
+ return true;
28
+ }
29
+ if (/^[0-9a-fA-F-]{16,}$/.test(segment) && segment.indexOf('-') >= 0) {
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ var defaultRouteNormalizer = function defaultRouteNormalizer(url) {
35
+ var path = extractPath(url);
36
+ var parts = path.split('/');
37
+ var normalized = [];
38
+ var i;
39
+ var part;
40
+ for (i = 0; i < parts.length; i += 1) {
41
+ part = parts[i];
42
+ if (part === '') {
43
+ if (i === 0) {
44
+ normalized.push('');
45
+ }
46
+ continue;
47
+ }
48
+ normalized.push(looksLikeId(part) ? ':id' : part);
49
+ }
50
+ path = normalized.join('/');
51
+ return path.charAt(0) === '/' ? path : '/' + path;
52
+ };
53
+ exports.defaultRouteNormalizer = defaultRouteNormalizer;
@@ -0,0 +1,6 @@
1
+ import { NetworkDiagnosis, NetworkRequestEvent } from './types';
2
+ export type RuleEngineOptions = {
3
+ minimumSamplesToDiagnose: number;
4
+ slowRequestThresholdMs: number;
5
+ };
6
+ export declare function diagnoseNetwork(events: NetworkRequestEvent[], options: RuleEngineOptions): NetworkDiagnosis;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diagnoseNetwork = void 0;
4
+ var metricsStore_1 = require("./metricsStore");
5
+ function countServerErrors(events) {
6
+ var count = 0;
7
+ var i;
8
+ var status;
9
+ for (i = 0; i < events.length; i += 1) {
10
+ status = events[i].status;
11
+ if (typeof status === 'number' && status >= 500) {
12
+ count += 1;
13
+ }
14
+ }
15
+ return count;
16
+ }
17
+ function countSlowRoutes(events, slowRequestThresholdMs) {
18
+ var routes = {};
19
+ var count = 0;
20
+ var i;
21
+ var route;
22
+ for (i = 0; i < events.length; i += 1) {
23
+ if (events[i].durationMs >= slowRequestThresholdMs) {
24
+ route = events[i].normalizedRoute;
25
+ if (!routes[route]) {
26
+ routes[route] = true;
27
+ count += 1;
28
+ }
29
+ }
30
+ }
31
+ return count;
32
+ }
33
+ function countRoutes(events) {
34
+ var routes = {};
35
+ var count = 0;
36
+ var i;
37
+ var route;
38
+ for (i = 0; i < events.length; i += 1) {
39
+ route = events[i].normalizedRoute;
40
+ if (!routes[route]) {
41
+ routes[route] = true;
42
+ count += 1;
43
+ }
44
+ }
45
+ return count;
46
+ }
47
+ function buildDiagnosis(status, probableCause, confidenceLevel, reasons, events, slowRequestThresholdMs) {
48
+ return {
49
+ status: status,
50
+ probableCause: probableCause,
51
+ confidenceLevel: confidenceLevel,
52
+ reasons: reasons,
53
+ summary: (0, metricsStore_1.calculateSummary)(events, slowRequestThresholdMs),
54
+ };
55
+ }
56
+ function diagnoseNetwork(events, options) {
57
+ var summary = (0, metricsStore_1.calculateSummary)(events, options.slowRequestThresholdMs);
58
+ var serverErrorRate;
59
+ var slowRouteCount;
60
+ var routeCount;
61
+ if (summary.requestCount < options.minimumSamplesToDiagnose) {
62
+ return buildDiagnosis('unknown', 'unknown', 'low', ['Not enough samples to diagnose network communication.'], events, options.slowRequestThresholdMs);
63
+ }
64
+ serverErrorRate = countServerErrors(events) / summary.requestCount;
65
+ slowRouteCount = countSlowRoutes(events, options.slowRequestThresholdMs);
66
+ routeCount = countRoutes(events);
67
+ if (summary.timeoutRate >= 0.5) {
68
+ return buildDiagnosis('poor', 'client-network', 'high', ['Many requests are timing out.'], events, options.slowRequestThresholdMs);
69
+ }
70
+ if (serverErrorRate >= 0.4) {
71
+ return buildDiagnosis('poor', 'backend', 'high', ['Many requests are failing with 5xx server errors.'], events, options.slowRequestThresholdMs);
72
+ }
73
+ if (summary.slowRequestRate >= 0.5 && summary.affectedEndpointRatio >= 0.5 && slowRouteCount > 1) {
74
+ return buildDiagnosis(summary.slowRequestRate >= 0.75 ? 'poor' : 'degraded', 'infrastructure', 'medium', ['Several different endpoints are slow in the current sample window.'], events, options.slowRequestThresholdMs);
75
+ }
76
+ if (summary.slowRequestRate >= 0.3 && slowRouteCount === 1 && routeCount > 1) {
77
+ return buildDiagnosis('degraded', 'specific-endpoint', 'medium', ['Only one endpoint concentrates slow requests.'], events, options.slowRequestThresholdMs);
78
+ }
79
+ if (summary.errorRate >= 0.3) {
80
+ return buildDiagnosis('degraded', 'unknown', 'medium', ['A relevant share of requests is failing.'], events, options.slowRequestThresholdMs);
81
+ }
82
+ return buildDiagnosis('good', 'unknown', 'high', ['Most requests in the current sample window are healthy.'], events, options.slowRequestThresholdMs);
83
+ }
84
+ exports.diagnoseNetwork = diagnoseNetwork;
@@ -0,0 +1,73 @@
1
+ export type NetworkRequestSource = 'fetch' | 'axios';
2
+ export type NetworkErrorType = 'timeout' | 'aborted' | 'network' | 'http-client' | 'http-server' | 'unknown';
3
+ export type NetworkRequestEvent = {
4
+ requestId: string;
5
+ source: NetworkRequestSource;
6
+ method: string;
7
+ url?: string;
8
+ route: string;
9
+ normalizedRoute: string;
10
+ startedAt: number;
11
+ finishedAt: number;
12
+ durationMs: number;
13
+ status?: number;
14
+ success: boolean;
15
+ errorType?: NetworkErrorType;
16
+ timedOut: boolean;
17
+ aborted: boolean;
18
+ };
19
+ export type NetworkStatus = 'good' | 'degraded' | 'poor' | 'offline' | 'unknown';
20
+ export type ProbableCause = 'client-network' | 'specific-endpoint' | 'backend' | 'infrastructure' | 'frontend-or-device' | 'unknown';
21
+ export type ConfidenceLevel = 'low' | 'medium' | 'high';
22
+ export type NetworkSummary = {
23
+ requestCount: number;
24
+ errorRate: number;
25
+ timeoutRate: number;
26
+ slowRequestRate: number;
27
+ medianDurationMs: number;
28
+ p95DurationMs: number;
29
+ affectedEndpointRatio: number;
30
+ };
31
+ export type NetworkDiagnosis = {
32
+ status: NetworkStatus;
33
+ probableCause: ProbableCause;
34
+ confidenceLevel: ConfidenceLevel;
35
+ reasons: string[];
36
+ summary: NetworkSummary;
37
+ };
38
+ export type RouteNormalizer = (url: string) => string;
39
+ export type NetworkListenerOptions = {
40
+ slowRequestThresholdMs?: number;
41
+ maxSamples?: number;
42
+ minimumSamplesToDiagnose?: number;
43
+ routeNormalizer?: RouteNormalizer;
44
+ timeoutThresholdMs?: number;
45
+ };
46
+ export type NetworkListener = {
47
+ start: () => void;
48
+ stop: () => void;
49
+ subscribe: (subscriber: NetworkDiagnosisSubscriber) => Unsubscribe;
50
+ getSnapshot: () => NetworkDiagnosis;
51
+ record: (event: NetworkRequestEvent) => void;
52
+ installFetch: (target?: FetchTarget) => Uninstall;
53
+ installAxios: (axiosInstance: AxiosLike) => Uninstall;
54
+ };
55
+ export type NetworkDiagnosisSubscriber = (diagnosis: NetworkDiagnosis) => void;
56
+ export type Unsubscribe = () => void;
57
+ export type Uninstall = () => void;
58
+ export type FetchTarget = {
59
+ fetch?: FetchLike;
60
+ };
61
+ export type FetchLike = {
62
+ apply: (thisArg: any, args: IArguments | any[]) => Promise<any>;
63
+ };
64
+ export type AxiosLike = {
65
+ interceptors: {
66
+ request: AxiosInterceptorManager;
67
+ response: AxiosInterceptorManager;
68
+ };
69
+ };
70
+ export type AxiosInterceptorManager = {
71
+ use: (onFulfilled?: any, onRejected?: any) => number;
72
+ eject: (id: number) => void;
73
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export { createNetworkListener } from './core/createNetworkListener';
2
+ export { defaultRouteNormalizer } from './core/routeNormalizer';
3
+ export { createMetricsStore, calculateSummary } from './core/metricsStore';
4
+ export { diagnoseNetwork } from './core/ruleEngine';
5
+ export * from './core/types';
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.diagnoseNetwork = exports.calculateSummary = exports.createMetricsStore = exports.defaultRouteNormalizer = exports.createNetworkListener = void 0;
18
+ var createNetworkListener_1 = require("./core/createNetworkListener");
19
+ Object.defineProperty(exports, "createNetworkListener", { enumerable: true, get: function () { return createNetworkListener_1.createNetworkListener; } });
20
+ var routeNormalizer_1 = require("./core/routeNormalizer");
21
+ Object.defineProperty(exports, "defaultRouteNormalizer", { enumerable: true, get: function () { return routeNormalizer_1.defaultRouteNormalizer; } });
22
+ var metricsStore_1 = require("./core/metricsStore");
23
+ Object.defineProperty(exports, "createMetricsStore", { enumerable: true, get: function () { return metricsStore_1.createMetricsStore; } });
24
+ Object.defineProperty(exports, "calculateSummary", { enumerable: true, get: function () { return metricsStore_1.calculateSummary; } });
25
+ var ruleEngine_1 = require("./core/ruleEngine");
26
+ Object.defineProperty(exports, "diagnoseNetwork", { enumerable: true, get: function () { return ruleEngine_1.diagnoseNetwork; } });
27
+ __exportStar(require("./core/types"), exports);
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@henrique-olivier/network-listener",
3
+ "version": "0.1.0",
4
+ "description": "POC de uma biblioteca leve para observar requisicoes HTTP e gerar diagnosticos provaveis.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "test": "jest --runInBand"
14
+ },
15
+ "keywords": [
16
+ "network",
17
+ "observability",
18
+ "frontend",
19
+ "fetch",
20
+ "axios"
21
+ ],
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@types/jest": "26.0.24",
25
+ "jest": "26.6.3",
26
+ "ts-jest": "26.5.6",
27
+ "typescript": "4.9.5"
28
+ }
29
+ }