adonisjs-server-stats 1.10.0 → 1.10.3
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 +23 -14
- package/dist/core/config-utils.d.ts +8 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/dashboard-data-controller.d.ts +16 -0
- package/dist/core/dashboard-data-helpers.d.ts +12 -0
- package/dist/core/debug-data-controller.d.ts +4 -0
- package/dist/core/define-config-helpers.d.ts +25 -0
- package/dist/core/feature-detect-helpers.d.ts +36 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/index.js +594 -509
- package/dist/core/log-utils-helpers.d.ts +13 -0
- package/dist/core/metrics.d.ts +3 -28
- package/dist/core/pagination.d.ts +0 -9
- package/dist/core/server-stats-controller.d.ts +6 -0
- package/dist/core/transmit-helpers.d.ts +7 -0
- package/dist/core/types-dashboard.d.ts +178 -0
- package/dist/core/types-diagnostics.d.ts +85 -0
- package/dist/core/types.d.ts +10 -442
- package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
- package/dist/react/CacheTab-2cw_rMzj.js +117 -0
- package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
- package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
- package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
- package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
- package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
- package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
- package/dist/react/EventsTab-CfVr7AiM.js +57 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
- package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
- package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
- package/dist/react/LogsTab-D8unMV5P.js +108 -0
- package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
- package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
- package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
- package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
- package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
- package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
- package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
- package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
- package/dist/react/index-CsImORX6.js +1121 -0
- package/dist/react/index.js +1 -1
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/app_collector.d.ts +0 -8
- package/dist/src/collectors/app_collector.js +45 -52
- package/dist/src/collectors/auto_detect.d.ts +0 -23
- package/dist/src/collectors/auto_detect.js +33 -55
- package/dist/src/collectors/db_pool_collector.d.ts +14 -16
- package/dist/src/collectors/db_pool_collector.js +72 -57
- package/dist/src/collectors/log_collector.d.ts +0 -47
- package/dist/src/collectors/log_collector.js +36 -65
- package/dist/src/collectors/queue_collector.d.ts +0 -20
- package/dist/src/collectors/queue_collector.js +60 -76
- package/dist/src/collectors/redis_collector.d.ts +10 -10
- package/dist/src/collectors/redis_collector.js +69 -66
- package/dist/src/config/deprecation_migration.d.ts +7 -0
- package/dist/src/config/deprecation_migration.js +201 -0
- package/dist/src/controller/debug_controller.d.ts +1 -1
- package/dist/src/controller/debug_controller.js +87 -81
- package/dist/src/dashboard/cache_handlers.d.ts +14 -0
- package/dist/src/dashboard/cache_handlers.js +52 -0
- package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
- package/dist/src/dashboard/chart_aggregator.js +68 -50
- package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
- package/dist/src/dashboard/coalesce_cache.js +47 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
- package/dist/src/dashboard/dashboard_controller.js +51 -544
- package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
- package/dist/src/dashboard/dashboard_page_assets.js +51 -0
- package/dist/src/dashboard/dashboard_store.d.ts +19 -218
- package/dist/src/dashboard/dashboard_store.js +115 -1116
- package/dist/src/dashboard/dashboard_types.d.ts +83 -0
- package/dist/src/dashboard/dashboard_types.js +4 -0
- package/dist/src/dashboard/detail_queries.d.ts +19 -0
- package/dist/src/dashboard/detail_queries.js +98 -0
- package/dist/src/dashboard/email_event_builder.d.ts +8 -0
- package/dist/src/dashboard/email_event_builder.js +65 -0
- package/dist/src/dashboard/explain_query.d.ts +8 -0
- package/dist/src/dashboard/explain_query.js +22 -0
- package/dist/src/dashboard/filter_handlers.d.ts +23 -0
- package/dist/src/dashboard/filter_handlers.js +56 -0
- package/dist/src/dashboard/filtered_queries.d.ts +15 -0
- package/dist/src/dashboard/filtered_queries.js +155 -0
- package/dist/src/dashboard/flush_manager.d.ts +25 -0
- package/dist/src/dashboard/flush_manager.js +107 -0
- package/dist/src/dashboard/format_helpers.d.ts +126 -0
- package/dist/src/dashboard/format_helpers.js +140 -0
- package/dist/src/dashboard/inspector_manager.d.ts +36 -0
- package/dist/src/dashboard/inspector_manager.js +102 -0
- package/dist/src/dashboard/integrations/config_inspector.js +11 -13
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
- package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
- package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
- package/dist/src/dashboard/jobs_handlers.js +61 -0
- package/dist/src/dashboard/knex_factory.d.ts +18 -0
- package/dist/src/dashboard/knex_factory.js +91 -0
- package/dist/src/dashboard/migrator.js +30 -159
- package/dist/src/dashboard/migrator_tables.d.ts +19 -0
- package/dist/src/dashboard/migrator_tables.js +153 -0
- package/dist/src/dashboard/overview_queries.d.ts +66 -0
- package/dist/src/dashboard/overview_queries.js +155 -0
- package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
- package/dist/src/dashboard/overview_query_runners.js +84 -0
- package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
- package/dist/src/dashboard/overview_store_queries.js +69 -0
- package/dist/src/dashboard/paginate_helper.d.ts +12 -0
- package/dist/src/dashboard/paginate_helper.js +33 -0
- package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
- package/dist/src/dashboard/query_explain_handler.js +80 -0
- package/dist/src/dashboard/read_queries.d.ts +32 -0
- package/dist/src/dashboard/read_queries.js +107 -0
- package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
- package/dist/src/dashboard/saved_filter_queries.js +24 -0
- package/dist/src/dashboard/storage_stats.d.ts +41 -0
- package/dist/src/dashboard/storage_stats.js +81 -0
- package/dist/src/dashboard/write_queue.d.ts +106 -0
- package/dist/src/dashboard/write_queue.js +225 -0
- package/dist/src/data/data_access.d.ts +2 -39
- package/dist/src/data/data_access.js +17 -193
- package/dist/src/data/data_access_helpers.d.ts +130 -0
- package/dist/src/data/data_access_helpers.js +212 -0
- package/dist/src/debug/debug_store.js +37 -32
- package/dist/src/debug/email_collector.d.ts +1 -10
- package/dist/src/debug/email_collector.js +78 -81
- package/dist/src/debug/event_collector.d.ts +0 -9
- package/dist/src/debug/event_collector.js +79 -62
- package/dist/src/debug/query_collector.js +23 -19
- package/dist/src/debug/route_inspector.d.ts +1 -5
- package/dist/src/debug/route_inspector.js +50 -51
- package/dist/src/debug/trace_collector.d.ts +9 -1
- package/dist/src/debug/trace_collector.js +21 -15
- package/dist/src/debug/types.d.ts +1 -1
- package/dist/src/define_config.d.ts +0 -65
- package/dist/src/define_config.js +93 -333
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/edge/plugin.d.ts +0 -16
- package/dist/src/edge/plugin.js +57 -64
- package/dist/src/engine/request_metrics.d.ts +1 -0
- package/dist/src/engine/request_metrics.js +32 -42
- package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -93
- package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
- package/dist/src/provider/auth_middleware_detector.js +97 -0
- package/dist/src/provider/boot_helpers.d.ts +20 -0
- package/dist/src/provider/boot_helpers.js +91 -0
- package/dist/src/provider/boot_initializer.d.ts +28 -0
- package/dist/src/provider/boot_initializer.js +35 -0
- package/dist/src/provider/dashboard_init.d.ts +30 -0
- package/dist/src/provider/dashboard_init.js +138 -0
- package/dist/src/provider/dashboard_setup.d.ts +25 -0
- package/dist/src/provider/dashboard_setup.js +78 -0
- package/dist/src/provider/diagnostics.d.ts +134 -0
- package/dist/src/provider/diagnostics.js +127 -0
- package/dist/src/provider/email_bridge.d.ts +43 -0
- package/dist/src/provider/email_bridge.js +80 -0
- package/dist/src/provider/email_helpers.d.ts +13 -0
- package/dist/src/provider/email_helpers.js +68 -0
- package/dist/src/provider/pino_hook.d.ts +17 -0
- package/dist/src/provider/pino_hook.js +35 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
- package/dist/src/provider/provider_helpers_extra.js +177 -0
- package/dist/src/provider/server_stats_provider.d.ts +39 -85
- package/dist/src/provider/server_stats_provider.js +132 -951
- package/dist/src/provider/shutdown_helpers.d.ts +43 -0
- package/dist/src/provider/shutdown_helpers.js +70 -0
- package/dist/src/provider/toolbar_setup.d.ts +57 -0
- package/dist/src/provider/toolbar_setup.js +141 -0
- package/dist/src/routes/dashboard_routes.d.ts +14 -0
- package/dist/src/routes/dashboard_routes.js +197 -0
- package/dist/src/routes/debug_routes.d.ts +14 -0
- package/dist/src/routes/debug_routes.js +101 -0
- package/dist/src/routes/register_routes.d.ts +0 -78
- package/dist/src/routes/register_routes.js +22 -352
- package/dist/src/routes/stats_routes.d.ts +5 -0
- package/dist/src/routes/stats_routes.js +14 -0
- package/dist/src/styles/components.css +96 -0
- package/dist/src/styles/dashboard.css +8 -90
- package/dist/src/styles/debug-panel.css +1 -31
- package/dist/src/types.d.ts +305 -14
- package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
- package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
- package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
- package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
- package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
- package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
- package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
- package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
- package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-C8MxnS7Q.js +1232 -0
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/CacheTab-CA8LB1J5.js +0 -123
- package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
- package/dist/react/EmailsTab-BDhEiomM.js +0 -153
- package/dist/react/EventsTab-CMfY98Rl.js +0 -63
- package/dist/react/LogsTab-CicucmVk.js +0 -103
- package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
- package/dist/react/index-Cflz9Ebj.js +0 -1069
- package/dist/vue/index-Dtgysd26.js +0 -1229
|
@@ -1,58 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { parseAndEnrich } from '../log_stream/log_stream_service.js';
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Helpers
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
/**
|
|
7
|
-
* Wrap a plain array in the standard {@link PaginatedResult} envelope.
|
|
8
|
-
*
|
|
9
|
-
* Applies optional client-side search filtering and pagination so that
|
|
10
|
-
* ring-buffer results match the same shape returned by the dashboard store.
|
|
11
|
-
*/
|
|
12
|
-
function wrapArray(items, opts, searchFn) {
|
|
13
|
-
let filtered = items;
|
|
14
|
-
// Client-side search
|
|
15
|
-
if (opts.search && searchFn) {
|
|
16
|
-
const term = opts.search.toLowerCase();
|
|
17
|
-
filtered = filtered.filter((item) => searchFn(item, term));
|
|
18
|
-
}
|
|
19
|
-
const total = filtered.length;
|
|
20
|
-
const page = opts.page ?? 1;
|
|
21
|
-
// When perPage is not specified, return all items (backward compat for debug panel)
|
|
22
|
-
const perPage = opts.perPage ?? (total || 1);
|
|
23
|
-
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
24
|
-
const start = (page - 1) * perPage;
|
|
25
|
-
const data = filtered.slice(start, start + perPage);
|
|
26
|
-
return {
|
|
27
|
-
data,
|
|
28
|
-
meta: { total, page, perPage, lastPage },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Convert a flat {@link DashboardStore.PaginatedResult} to the nested
|
|
33
|
-
* `{ data, meta }` shape used by the unified API.
|
|
34
|
-
*/
|
|
35
|
-
function fromDashboardResult(result) {
|
|
36
|
-
return {
|
|
37
|
-
data: result.data,
|
|
38
|
-
meta: {
|
|
39
|
-
total: result.total,
|
|
40
|
-
page: result.page,
|
|
41
|
-
perPage: result.perPage,
|
|
42
|
-
lastPage: result.lastPage,
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function mapTraceListRow(row) {
|
|
47
|
-
return {
|
|
48
|
-
...row,
|
|
49
|
-
requestId: row.request_id ?? row.requestId,
|
|
50
|
-
statusCode: row.status_code ?? row.statusCode,
|
|
51
|
-
totalDuration: row.total_duration ?? row.totalDuration,
|
|
52
|
-
spanCount: row.span_count ?? row.spanCount,
|
|
53
|
-
createdAt: row.created_at ?? row.createdAt,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
1
|
+
import { wrapArray, fromDashboardResult, mapTraceListRow, normalizeEmailRow, buildPaginationArgs, buildQueryFilters, buildEventFilters, buildEmailFilters, buildTraceFilters, buildLogFilters, stripEmailForList, stripTraceForList, filterRoutes, readLogFile, } from './data_access_helpers.js';
|
|
56
2
|
// ---------------------------------------------------------------------------
|
|
57
3
|
// DataAccess
|
|
58
4
|
// ---------------------------------------------------------------------------
|
|
@@ -87,13 +33,8 @@ export class DataAccess {
|
|
|
87
33
|
// =========================================================================
|
|
88
34
|
async getQueries(opts = {}) {
|
|
89
35
|
if (this.hasPersistence && opts.source !== 'memory') {
|
|
90
|
-
const page = opts
|
|
91
|
-
const
|
|
92
|
-
const filters = {
|
|
93
|
-
search: opts.search,
|
|
94
|
-
...opts.filters,
|
|
95
|
-
};
|
|
96
|
-
const result = await this.dashboardStore.getQueries(page, perPage, filters);
|
|
36
|
+
const { page, perPage } = buildPaginationArgs(opts);
|
|
37
|
+
const result = await this.dashboardStore.getQueries(page, perPage, buildQueryFilters(opts));
|
|
97
38
|
return fromDashboardResult(result);
|
|
98
39
|
}
|
|
99
40
|
const queries = this.debugStore.queries.getQueries();
|
|
@@ -111,13 +52,8 @@ export class DataAccess {
|
|
|
111
52
|
// =========================================================================
|
|
112
53
|
async getEvents(opts = {}) {
|
|
113
54
|
if (this.hasPersistence && opts.source !== 'memory') {
|
|
114
|
-
const page = opts
|
|
115
|
-
const
|
|
116
|
-
const filters = {
|
|
117
|
-
search: opts.search,
|
|
118
|
-
...opts.filters,
|
|
119
|
-
};
|
|
120
|
-
const result = await this.dashboardStore.getEvents(page, perPage, filters);
|
|
55
|
+
const { page, perPage } = buildPaginationArgs(opts);
|
|
56
|
+
const result = await this.dashboardStore.getEvents(page, perPage, buildEventFilters(opts));
|
|
121
57
|
return fromDashboardResult(result);
|
|
122
58
|
}
|
|
123
59
|
const events = this.debugStore.events.getEvents();
|
|
@@ -136,42 +72,14 @@ export class DataAccess {
|
|
|
136
72
|
*/
|
|
137
73
|
async getEmails(opts = {}) {
|
|
138
74
|
if (this.hasPersistence && opts.source !== 'memory') {
|
|
139
|
-
const page = opts
|
|
140
|
-
const
|
|
141
|
-
const filters = {
|
|
142
|
-
search: opts.search,
|
|
143
|
-
...opts.filters,
|
|
144
|
-
};
|
|
145
|
-
const result = await this.dashboardStore.getEmails(page, perPage, filters, true);
|
|
75
|
+
const { page, perPage } = buildPaginationArgs(opts);
|
|
76
|
+
const result = await this.dashboardStore.getEmails(page, perPage, buildEmailFilters(opts), true);
|
|
146
77
|
const normalized = fromDashboardResult(result);
|
|
147
|
-
|
|
148
|
-
// so both memory and SQLite paths return consistent field names
|
|
149
|
-
normalized.data = normalized.data.map((row) => ({
|
|
150
|
-
...row,
|
|
151
|
-
from: row.from_addr ?? row.from ?? '',
|
|
152
|
-
to: row.to_addr ?? row.to ?? '',
|
|
153
|
-
messageId: row.message_id ?? row.messageId ?? null,
|
|
154
|
-
attachmentCount: row.attachment_count ?? row.attachmentCount ?? 0,
|
|
155
|
-
timestamp: row.created_at ?? row.timestamp ?? null,
|
|
156
|
-
}));
|
|
78
|
+
normalized.data = normalized.data.map(normalizeEmailRow);
|
|
157
79
|
return normalized;
|
|
158
80
|
}
|
|
159
81
|
const emails = this.debugStore.emails.getEmails();
|
|
160
|
-
|
|
161
|
-
// without object-spread to avoid copying large HTML bodies
|
|
162
|
-
const stripped = emails.map((e) => ({
|
|
163
|
-
id: e.id,
|
|
164
|
-
from: e.from,
|
|
165
|
-
to: e.to,
|
|
166
|
-
cc: e.cc,
|
|
167
|
-
bcc: e.bcc,
|
|
168
|
-
subject: e.subject,
|
|
169
|
-
mailer: e.mailer,
|
|
170
|
-
status: e.status,
|
|
171
|
-
messageId: e.messageId,
|
|
172
|
-
attachmentCount: e.attachmentCount,
|
|
173
|
-
timestamp: e.timestamp,
|
|
174
|
-
}));
|
|
82
|
+
const stripped = emails.map(stripEmailForList);
|
|
175
83
|
return wrapArray(stripped, opts, (e, term) => {
|
|
176
84
|
return (e.from.toLowerCase().includes(term) ||
|
|
177
85
|
e.to.toLowerCase().includes(term) ||
|
|
@@ -201,13 +109,8 @@ export class DataAccess {
|
|
|
201
109
|
*/
|
|
202
110
|
async getTraces(opts = {}) {
|
|
203
111
|
if (this.hasPersistence && opts.source !== 'memory') {
|
|
204
|
-
const page = opts
|
|
205
|
-
const
|
|
206
|
-
const filters = {
|
|
207
|
-
search: opts.search,
|
|
208
|
-
...opts.filters,
|
|
209
|
-
};
|
|
210
|
-
const result = await this.dashboardStore.getTraces(page, perPage, filters);
|
|
112
|
+
const { page, perPage } = buildPaginationArgs(opts);
|
|
113
|
+
const result = await this.dashboardStore.getTraces(page, perPage, buildTraceFilters(opts));
|
|
211
114
|
return {
|
|
212
115
|
...fromDashboardResult(result),
|
|
213
116
|
data: result.data.map(mapTraceListRow),
|
|
@@ -217,18 +120,7 @@ export class DataAccess {
|
|
|
217
120
|
return { data: [], meta: { total: 0, page: 1, perPage: opts.perPage ?? 50, lastPage: 1 } };
|
|
218
121
|
}
|
|
219
122
|
const traces = this.debugStore.traces.getTraces();
|
|
220
|
-
|
|
221
|
-
// objects without spread to avoid copying large span arrays
|
|
222
|
-
const list = traces.map((t) => ({
|
|
223
|
-
id: t.id,
|
|
224
|
-
method: t.method,
|
|
225
|
-
url: t.url,
|
|
226
|
-
statusCode: t.statusCode,
|
|
227
|
-
totalDuration: t.totalDuration,
|
|
228
|
-
spanCount: t.spanCount,
|
|
229
|
-
warningCount: t.warnings.length,
|
|
230
|
-
timestamp: t.timestamp,
|
|
231
|
-
}));
|
|
123
|
+
const list = traces.map(stripTraceForList);
|
|
232
124
|
return wrapArray(list, opts, (t, term) => {
|
|
233
125
|
return t.method.toLowerCase().includes(term) || t.url.toLowerCase().includes(term);
|
|
234
126
|
});
|
|
@@ -258,12 +150,8 @@ export class DataAccess {
|
|
|
258
150
|
}
|
|
259
151
|
/**
|
|
260
152
|
* Find log entries matching a specific request ID.
|
|
261
|
-
*
|
|
262
|
-
* Checks SQLite first (if available), then falls back to scanning
|
|
263
|
-
* the log file for entries with a matching `request_id` field.
|
|
264
153
|
*/
|
|
265
154
|
async getRelatedLogsByRequestId(requestId) {
|
|
266
|
-
// Try SQLite first
|
|
267
155
|
if (this.hasPersistence) {
|
|
268
156
|
try {
|
|
269
157
|
const result = await this.dashboardStore.getLogs(1, 50, { requestId });
|
|
@@ -273,8 +161,7 @@ export class DataAccess {
|
|
|
273
161
|
// Fall through to log file
|
|
274
162
|
}
|
|
275
163
|
}
|
|
276
|
-
|
|
277
|
-
const entries = await this.readLogFile();
|
|
164
|
+
const entries = this.logPath ? await readLogFile(this.logPath) : [];
|
|
278
165
|
return entries.filter((e) => e.request_id === requestId || e.requestId === requestId);
|
|
279
166
|
}
|
|
280
167
|
// =========================================================================
|
|
@@ -289,17 +176,7 @@ export class DataAccess {
|
|
|
289
176
|
getRoutes(search) {
|
|
290
177
|
let routes = this.debugStore.routes.getRoutes();
|
|
291
178
|
if (search) {
|
|
292
|
-
|
|
293
|
-
routes = routes.filter((r) => {
|
|
294
|
-
const pattern = (r.pattern || '').toLowerCase();
|
|
295
|
-
const handler = (r.handler || '').toLowerCase();
|
|
296
|
-
const name = (r.name || '').toLowerCase();
|
|
297
|
-
const method = (r.method || '').toLowerCase();
|
|
298
|
-
return (pattern.includes(term) ||
|
|
299
|
-
handler.includes(term) ||
|
|
300
|
-
name.includes(term) ||
|
|
301
|
-
method.includes(term));
|
|
302
|
-
});
|
|
179
|
+
routes = filterRoutes(routes, search);
|
|
303
180
|
}
|
|
304
181
|
const total = routes.length;
|
|
305
182
|
return {
|
|
@@ -319,68 +196,15 @@ export class DataAccess {
|
|
|
319
196
|
*/
|
|
320
197
|
async getLogs(opts = {}) {
|
|
321
198
|
if (this.hasPersistence && opts.source !== 'memory') {
|
|
322
|
-
const page = opts
|
|
323
|
-
const
|
|
324
|
-
const filters = {
|
|
325
|
-
search: opts.search,
|
|
326
|
-
...opts.filters,
|
|
327
|
-
};
|
|
328
|
-
const result = await this.dashboardStore.getLogs(page, perPage, filters);
|
|
199
|
+
const { page, perPage } = buildPaginationArgs(opts);
|
|
200
|
+
const result = await this.dashboardStore.getLogs(page, perPage, buildLogFilters(opts));
|
|
329
201
|
return fromDashboardResult(result);
|
|
330
202
|
}
|
|
331
|
-
|
|
332
|
-
const entries = await this.readLogFile();
|
|
203
|
+
const entries = this.logPath ? await readLogFile(this.logPath) : [];
|
|
333
204
|
return wrapArray(entries, opts, (e, term) => {
|
|
334
205
|
const msg = String(e.msg ?? e.message ?? '').toLowerCase();
|
|
335
206
|
const levelName = String(e.levelName ?? '').toLowerCase();
|
|
336
207
|
return msg.includes(term) || levelName.includes(term);
|
|
337
208
|
});
|
|
338
209
|
}
|
|
339
|
-
// =========================================================================
|
|
340
|
-
// Private helpers
|
|
341
|
-
// =========================================================================
|
|
342
|
-
/**
|
|
343
|
-
* Read and parse the last 256 KB of the application log file.
|
|
344
|
-
*
|
|
345
|
-
* Returns an array of enriched log entry objects. If the log file
|
|
346
|
-
* does not exist or cannot be read, returns an empty array.
|
|
347
|
-
*/
|
|
348
|
-
async readLogFile() {
|
|
349
|
-
if (!this.logPath)
|
|
350
|
-
return [];
|
|
351
|
-
try {
|
|
352
|
-
const stats = await stat(this.logPath);
|
|
353
|
-
const maxBytes = 256 * 1024;
|
|
354
|
-
let content;
|
|
355
|
-
if (stats.size > maxBytes) {
|
|
356
|
-
const { createReadStream } = await import('node:fs');
|
|
357
|
-
const stream = createReadStream(this.logPath, {
|
|
358
|
-
start: stats.size - maxBytes,
|
|
359
|
-
encoding: 'utf-8',
|
|
360
|
-
});
|
|
361
|
-
const chunks = [];
|
|
362
|
-
for await (const chunk of stream) {
|
|
363
|
-
chunks.push(chunk);
|
|
364
|
-
}
|
|
365
|
-
content = chunks.join('');
|
|
366
|
-
// Skip first potentially incomplete line
|
|
367
|
-
const firstNewline = content.indexOf('\n');
|
|
368
|
-
if (firstNewline !== -1)
|
|
369
|
-
content = content.slice(firstNewline + 1);
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
content = await readFile(this.logPath, 'utf-8');
|
|
373
|
-
}
|
|
374
|
-
return content
|
|
375
|
-
.trim()
|
|
376
|
-
.split('\n')
|
|
377
|
-
.filter(Boolean)
|
|
378
|
-
.map((line) => parseAndEnrich(line))
|
|
379
|
-
.filter((entry) => entry !== null)
|
|
380
|
-
.reverse();
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
return [];
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
210
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { DashboardStore, QueryFilters, EventFilters, EmailFilters, TraceFilters, LogFilters } from '../dashboard/dashboard_store.js';
|
|
2
|
+
import type { DebugStore } from '../debug/debug_store.js';
|
|
3
|
+
import type { QueryRecord, EventRecord, TraceRecord, RouteRecord } from '../debug/types.js';
|
|
4
|
+
export interface ListOptions {
|
|
5
|
+
page?: number;
|
|
6
|
+
perPage?: number;
|
|
7
|
+
search?: string;
|
|
8
|
+
sort?: string;
|
|
9
|
+
sortDir?: 'asc' | 'desc';
|
|
10
|
+
filters?: Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Force the data source for this read.
|
|
13
|
+
*
|
|
14
|
+
* - `'memory'` — always read from ring buffers ({@link DebugStore}).
|
|
15
|
+
* Use this for the debug panel, which expects camelCase field names
|
|
16
|
+
* matching the {@link QueryRecord}/{@link EventRecord}/etc. interfaces.
|
|
17
|
+
* - `'auto'` (default) — use SQLite when available, fall back to memory.
|
|
18
|
+
*/
|
|
19
|
+
source?: 'memory' | 'auto';
|
|
20
|
+
}
|
|
21
|
+
export interface PaginatedResult<T = Record<string, unknown>> {
|
|
22
|
+
data: T[];
|
|
23
|
+
meta: {
|
|
24
|
+
total: number;
|
|
25
|
+
page: number;
|
|
26
|
+
perPage: number;
|
|
27
|
+
lastPage: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a plain array in the standard {@link PaginatedResult} envelope.
|
|
32
|
+
*
|
|
33
|
+
* Applies optional client-side search filtering and pagination so that
|
|
34
|
+
* ring-buffer results match the same shape returned by the dashboard store.
|
|
35
|
+
*/
|
|
36
|
+
export declare function wrapArray<T>(items: T[], opts: ListOptions, searchFn?: (item: T, term: string) => boolean): PaginatedResult<T>;
|
|
37
|
+
/**
|
|
38
|
+
* Convert a flat {@link DashboardStore.PaginatedResult} to the nested
|
|
39
|
+
* `{ data, meta }` shape used by the unified API.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fromDashboardResult<T>(result: {
|
|
42
|
+
data: T[];
|
|
43
|
+
total: number;
|
|
44
|
+
page: number;
|
|
45
|
+
perPage: number;
|
|
46
|
+
lastPage: number;
|
|
47
|
+
}): PaginatedResult<T>;
|
|
48
|
+
export declare function mapTraceListRow<T extends Record<string, unknown>>(row: T): T & {
|
|
49
|
+
requestId: unknown;
|
|
50
|
+
statusCode: unknown;
|
|
51
|
+
totalDuration: unknown;
|
|
52
|
+
spanCount: unknown;
|
|
53
|
+
createdAt: unknown;
|
|
54
|
+
};
|
|
55
|
+
/** Normalize SQLite email column names to match the EmailRecord shape. */
|
|
56
|
+
export declare function normalizeEmailRow(row: Record<string, unknown>): {
|
|
57
|
+
from: unknown;
|
|
58
|
+
to: unknown;
|
|
59
|
+
messageId: unknown;
|
|
60
|
+
attachmentCount: unknown;
|
|
61
|
+
timestamp: unknown;
|
|
62
|
+
};
|
|
63
|
+
/** Build dashboard store query/event/email/trace pagination args. */
|
|
64
|
+
export declare function buildPaginationArgs(opts: ListOptions): {
|
|
65
|
+
page: number;
|
|
66
|
+
perPage: number;
|
|
67
|
+
};
|
|
68
|
+
export declare function buildQueryFilters(opts: ListOptions): QueryFilters;
|
|
69
|
+
export declare function buildEventFilters(opts: ListOptions): EventFilters;
|
|
70
|
+
export declare function buildEmailFilters(opts: ListOptions): EmailFilters;
|
|
71
|
+
export declare function buildTraceFilters(opts: ListOptions): TraceFilters;
|
|
72
|
+
export declare function buildLogFilters(opts: ListOptions): LogFilters;
|
|
73
|
+
/** Strip heavy html/text bodies from email records for list view. */
|
|
74
|
+
export declare function stripEmailForList(e: {
|
|
75
|
+
id: number;
|
|
76
|
+
from: string;
|
|
77
|
+
to: string;
|
|
78
|
+
cc?: string | null;
|
|
79
|
+
bcc?: string | null;
|
|
80
|
+
subject: string;
|
|
81
|
+
mailer: string;
|
|
82
|
+
status: string;
|
|
83
|
+
messageId?: string | null;
|
|
84
|
+
attachmentCount?: number;
|
|
85
|
+
timestamp?: string | number | null;
|
|
86
|
+
}): {
|
|
87
|
+
id: number;
|
|
88
|
+
from: string;
|
|
89
|
+
to: string;
|
|
90
|
+
cc: string | null | undefined;
|
|
91
|
+
bcc: string | null | undefined;
|
|
92
|
+
subject: string;
|
|
93
|
+
mailer: string;
|
|
94
|
+
status: string;
|
|
95
|
+
messageId: string | null | undefined;
|
|
96
|
+
attachmentCount: number | undefined;
|
|
97
|
+
timestamp: string | number | null | undefined;
|
|
98
|
+
};
|
|
99
|
+
/** Strip spans from trace records for list view. */
|
|
100
|
+
export declare function stripTraceForList(t: {
|
|
101
|
+
id: number;
|
|
102
|
+
method: string;
|
|
103
|
+
url: string;
|
|
104
|
+
statusCode: number;
|
|
105
|
+
totalDuration: number;
|
|
106
|
+
spanCount: number;
|
|
107
|
+
warnings: readonly unknown[];
|
|
108
|
+
timestamp?: string | number | null;
|
|
109
|
+
}): {
|
|
110
|
+
id: number;
|
|
111
|
+
method: string;
|
|
112
|
+
url: string;
|
|
113
|
+
statusCode: number;
|
|
114
|
+
totalDuration: number;
|
|
115
|
+
spanCount: number;
|
|
116
|
+
warningCount: number;
|
|
117
|
+
timestamp: string | number | null | undefined;
|
|
118
|
+
};
|
|
119
|
+
/** Filter routes by search term across pattern, handler, name, and method. */
|
|
120
|
+
export declare function filterRoutes(routes: RouteRecord[], search: string): RouteRecord[];
|
|
121
|
+
/**
|
|
122
|
+
* Read and parse the last 256 KB of a log file.
|
|
123
|
+
*
|
|
124
|
+
* Returns an array of enriched log entry objects. If the log file
|
|
125
|
+
* does not exist or cannot be read, returns an empty array.
|
|
126
|
+
*/
|
|
127
|
+
export declare function readLogFile(logPath: string): Promise<Record<string, unknown>[]>;
|
|
128
|
+
export type { DashboardStore, QueryFilters, EventFilters, EmailFilters, TraceFilters, LogFilters };
|
|
129
|
+
export type { DebugStore };
|
|
130
|
+
export type { QueryRecord, EventRecord, TraceRecord, RouteRecord };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { parseAndEnrich } from '../log_stream/log_stream_service.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Pagination helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Wrap a plain array in the standard {@link PaginatedResult} envelope.
|
|
8
|
+
*
|
|
9
|
+
* Applies optional client-side search filtering and pagination so that
|
|
10
|
+
* ring-buffer results match the same shape returned by the dashboard store.
|
|
11
|
+
*/
|
|
12
|
+
export function wrapArray(items, opts, searchFn) {
|
|
13
|
+
let filtered = items;
|
|
14
|
+
// Client-side search
|
|
15
|
+
if (opts.search && searchFn) {
|
|
16
|
+
const term = opts.search.toLowerCase();
|
|
17
|
+
filtered = filtered.filter((item) => searchFn(item, term));
|
|
18
|
+
}
|
|
19
|
+
const total = filtered.length;
|
|
20
|
+
const page = opts.page ?? 1;
|
|
21
|
+
// When perPage is not specified, return all items (backward compat for debug panel)
|
|
22
|
+
const perPage = opts.perPage ?? (total || 1);
|
|
23
|
+
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
24
|
+
const start = (page - 1) * perPage;
|
|
25
|
+
const data = filtered.slice(start, start + perPage);
|
|
26
|
+
return {
|
|
27
|
+
data,
|
|
28
|
+
meta: { total, page, perPage, lastPage },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert a flat {@link DashboardStore.PaginatedResult} to the nested
|
|
33
|
+
* `{ data, meta }` shape used by the unified API.
|
|
34
|
+
*/
|
|
35
|
+
export function fromDashboardResult(result) {
|
|
36
|
+
return {
|
|
37
|
+
data: result.data,
|
|
38
|
+
meta: {
|
|
39
|
+
total: result.total,
|
|
40
|
+
page: result.page,
|
|
41
|
+
perPage: result.perPage,
|
|
42
|
+
lastPage: result.lastPage,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Row mapping helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export function mapTraceListRow(row) {
|
|
50
|
+
return {
|
|
51
|
+
...row,
|
|
52
|
+
requestId: row.request_id ?? row.requestId,
|
|
53
|
+
statusCode: row.status_code ?? row.statusCode,
|
|
54
|
+
totalDuration: row.total_duration ?? row.totalDuration,
|
|
55
|
+
spanCount: row.span_count ?? row.spanCount,
|
|
56
|
+
createdAt: row.created_at ?? row.createdAt,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Pick the first defined value from a row, trying snake_case then camelCase. */
|
|
60
|
+
function pick(row, snake, camel, fallback = null) {
|
|
61
|
+
return row[snake] ?? row[camel] ?? fallback;
|
|
62
|
+
}
|
|
63
|
+
/** Normalize SQLite email column names to match the EmailRecord shape. */
|
|
64
|
+
export function normalizeEmailRow(row) {
|
|
65
|
+
return {
|
|
66
|
+
...row,
|
|
67
|
+
from: pick(row, 'from_addr', 'from', ''),
|
|
68
|
+
to: pick(row, 'to_addr', 'to', ''),
|
|
69
|
+
messageId: pick(row, 'message_id', 'messageId'),
|
|
70
|
+
attachmentCount: pick(row, 'attachment_count', 'attachmentCount', 0),
|
|
71
|
+
timestamp: pick(row, 'created_at', 'timestamp'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Store delegate helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/** Build dashboard store query/event/email/trace pagination args. */
|
|
78
|
+
export function buildPaginationArgs(opts) {
|
|
79
|
+
return {
|
|
80
|
+
page: opts.page ?? 1,
|
|
81
|
+
perPage: opts.perPage ?? 50,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function buildQueryFilters(opts) {
|
|
85
|
+
return {
|
|
86
|
+
search: opts.search,
|
|
87
|
+
...opts.filters,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function buildEventFilters(opts) {
|
|
91
|
+
return {
|
|
92
|
+
search: opts.search,
|
|
93
|
+
...opts.filters,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function buildEmailFilters(opts) {
|
|
97
|
+
return {
|
|
98
|
+
search: opts.search,
|
|
99
|
+
...opts.filters,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function buildTraceFilters(opts) {
|
|
103
|
+
return {
|
|
104
|
+
search: opts.search,
|
|
105
|
+
...opts.filters,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function buildLogFilters(opts) {
|
|
109
|
+
return {
|
|
110
|
+
search: opts.search,
|
|
111
|
+
...opts.filters,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Email stripping
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/** Strip heavy html/text bodies from email records for list view. */
|
|
118
|
+
export function stripEmailForList(e) {
|
|
119
|
+
return {
|
|
120
|
+
id: e.id,
|
|
121
|
+
from: e.from,
|
|
122
|
+
to: e.to,
|
|
123
|
+
cc: e.cc,
|
|
124
|
+
bcc: e.bcc,
|
|
125
|
+
subject: e.subject,
|
|
126
|
+
mailer: e.mailer,
|
|
127
|
+
status: e.status,
|
|
128
|
+
messageId: e.messageId,
|
|
129
|
+
attachmentCount: e.attachmentCount,
|
|
130
|
+
timestamp: e.timestamp,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Trace list stripping
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/** Strip spans from trace records for list view. */
|
|
137
|
+
export function stripTraceForList(t) {
|
|
138
|
+
return {
|
|
139
|
+
id: t.id,
|
|
140
|
+
method: t.method,
|
|
141
|
+
url: t.url,
|
|
142
|
+
statusCode: t.statusCode,
|
|
143
|
+
totalDuration: t.totalDuration,
|
|
144
|
+
spanCount: t.spanCount,
|
|
145
|
+
warningCount: t.warnings.length,
|
|
146
|
+
timestamp: t.timestamp,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Route search
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/** Filter routes by search term across pattern, handler, name, and method. */
|
|
153
|
+
export function filterRoutes(routes, search) {
|
|
154
|
+
const term = search.toLowerCase();
|
|
155
|
+
return routes.filter((r) => {
|
|
156
|
+
const pattern = (r.pattern || '').toLowerCase();
|
|
157
|
+
const handler = (r.handler || '').toLowerCase();
|
|
158
|
+
const name = (r.name || '').toLowerCase();
|
|
159
|
+
const method = (r.method || '').toLowerCase();
|
|
160
|
+
return (pattern.includes(term) ||
|
|
161
|
+
handler.includes(term) ||
|
|
162
|
+
name.includes(term) ||
|
|
163
|
+
method.includes(term));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Log file reader
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Read and parse the last 256 KB of a log file.
|
|
171
|
+
*
|
|
172
|
+
* Returns an array of enriched log entry objects. If the log file
|
|
173
|
+
* does not exist or cannot be read, returns an empty array.
|
|
174
|
+
*/
|
|
175
|
+
export async function readLogFile(logPath) {
|
|
176
|
+
try {
|
|
177
|
+
const stats = await stat(logPath);
|
|
178
|
+
const content = await readLogContent(logPath, stats.size);
|
|
179
|
+
return content
|
|
180
|
+
.trim()
|
|
181
|
+
.split('\n')
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
.map((line) => parseAndEnrich(line))
|
|
184
|
+
.filter((entry) => entry !== null)
|
|
185
|
+
.reverse();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Read the last maxBytes of a file, skipping partial first line if truncated. */
|
|
192
|
+
async function readLogContent(logPath, fileSize) {
|
|
193
|
+
const maxBytes = 256 * 1024;
|
|
194
|
+
if (fileSize <= maxBytes) {
|
|
195
|
+
return readFile(logPath, 'utf-8');
|
|
196
|
+
}
|
|
197
|
+
const { createReadStream } = await import('node:fs');
|
|
198
|
+
const stream = createReadStream(logPath, {
|
|
199
|
+
start: fileSize - maxBytes,
|
|
200
|
+
encoding: 'utf-8',
|
|
201
|
+
});
|
|
202
|
+
const chunks = [];
|
|
203
|
+
for await (const chunk of stream) {
|
|
204
|
+
chunks.push(chunk);
|
|
205
|
+
}
|
|
206
|
+
let content = chunks.join('');
|
|
207
|
+
// Skip first potentially incomplete line
|
|
208
|
+
const firstNewline = content.indexOf('\n');
|
|
209
|
+
if (firstNewline !== -1)
|
|
210
|
+
content = content.slice(firstNewline + 1);
|
|
211
|
+
return content;
|
|
212
|
+
}
|