UNPKG

@logtail/next

Version:

Better Stack Telemetry Next.js client

290 lines 12.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.prettyPrint = exports.log = exports.Logger = exports.LogLevel = void 0; const config_1 = require("./config"); const shared_1 = require("./shared"); const url = config_1.config.getLogsEndpoint(); const LOG_LEVEL = process.env.NEXT_PUBLIC_BETTER_STACK_LOG_LEVEL || 'debug'; var LogLevel; (function (LogLevel) { LogLevel[LogLevel["debug"] = 0] = "debug"; LogLevel[LogLevel["info"] = 1] = "info"; LogLevel[LogLevel["warn"] = 2] = "warn"; LogLevel[LogLevel["error"] = 3] = "error"; LogLevel[LogLevel["off"] = 100] = "off"; })(LogLevel || (exports.LogLevel = LogLevel = {})); class Logger { constructor(initConfig = {}) { this.initConfig = initConfig; this.logEvents = []; this.throttledSendLogs = (0, shared_1.throttle)(this.sendLogs, 1000); this.children = []; this.logLevel = LogLevel.debug; this.config = { autoFlush: true, source: 'frontend-log', prettyPrint: prettyPrint, }; this.debug = (message, args = {}) => { this.log(LogLevel.debug, message, args); }; this.info = (message, args = {}) => { this.log(LogLevel.info, message, args); }; this.warn = (message, args = {}) => { this.log(LogLevel.warn, message, args); }; this.error = (message, args = {}) => { this.log(LogLevel.error, message, args); }; this.with = (args) => { const config = Object.assign(Object.assign({}, this.config), { args: Object.assign(Object.assign({}, this.config.args), args) }); const child = new Logger(config); this.children.push(child); return child; }; this.withRequest = (req) => { return new Logger(Object.assign(Object.assign({}, this.config), { req: Object.assign(Object.assign({}, this.config.req), req) })); }; this._transformEvent = (level, message, args = {}) => { const logEvent = { level: LogLevel[level].toString(), message, dt: new Date(Date.now()).toISOString(), source: this.config.source, fields: this.config.args || {}, '@app': { 'next-logtail-version': config_1.Version, }, }; // check if passed args is an object, if its not an object, add it to fields.args if (args instanceof Error) { logEvent.fields = Object.assign(Object.assign({}, logEvent.fields), { message: args.message, stack: args.stack, name: args.name }); } else if (typeof args === 'object' && args !== null && Object.keys(args).length > 0) { const parsedArgs = JSON.parse(JSON.stringify(args, jsonFriendlyErrorReplacer)); logEvent.fields = Object.assign(Object.assign({}, logEvent.fields), parsedArgs); } else if (args && args.length) { logEvent.fields = Object.assign(Object.assign({}, logEvent.fields), { args: args }); } config_1.config.injectPlatformMetadata(logEvent, this.config.source); if (this.config.req != null) { logEvent.request = this.config.req; if (logEvent.platform) { logEvent.platform.route = this.config.req.path; } else if (logEvent.vercel) { logEvent.vercel.route = this.config.req.path; } } return logEvent; }; this.log = (level, message, args = {}) => { // Check log level before proceeding if (level < this.logLevel || this.logLevel === LogLevel.off) { return; } const logEvent = this._transformEvent(level, message, args); this.logEvents.push(logEvent); if (this.config.autoFlush) { this.throttledSendLogs(); } }; this.attachResponseStatus = (statusCode) => { this.logEvents = this.logEvents.map((log) => { if (log.request) { log.request.statusCode = statusCode; } return log; }); }; this.flush = () => __awaiter(this, void 0, void 0, function* () { yield Promise.all([this.sendLogs(), ...this.children.map((c) => c.flush())]); }); // check if user passed a log level, if not the default init value will be used as is. if (this.initConfig.logLevel != undefined && this.initConfig.logLevel >= 0) { this.logLevel = this.initConfig.logLevel; } else if (LOG_LEVEL) { this.logLevel = LogLevel[LOG_LEVEL]; } this.config = Object.assign(Object.assign({}, this.config), initConfig); } logHttpRequest(level, message, request, args) { // Check log level before proceeding if (level < this.logLevel || this.logLevel === LogLevel.off) { return; } const logEvent = this._transformEvent(level, message, args); logEvent.request = request; this.logEvents.push(logEvent); if (this.config.autoFlush) { this.throttledSendLogs(); } } middleware(request, config) { var _a; const req = { // @ts-ignore NextRequest.ip was removed in Next 15, works with undefined ip: request.ip, // @ts-ignore NextRequest.ip was removed in Next 15, works with undefined region: (_a = request.geo) === null || _a === void 0 ? void 0 : _a.region, method: request.method, host: request.nextUrl.hostname, path: request.nextUrl.pathname, scheme: request.nextUrl.protocol.split(':')[0], referer: request.headers.get('Referer'), userAgent: request.headers.get('user-agent'), }; const message = `${request.method} ${request.nextUrl.pathname}`; if (config === null || config === void 0 ? void 0 : config.logRequestDetails) { return (0, shared_1.requestToJSON)(request).then((details) => { const newReq = Object.assign(Object.assign({}, req), { details: Array.isArray(config.logRequestDetails) ? Object.fromEntries(Object.entries(details).filter(([key]) => config.logRequestDetails.includes(key))) : details }); return this.logHttpRequest(LogLevel.info, message, newReq, {}); }); } return this.logHttpRequest(LogLevel.info, message, req, {}); } sendLogs() { return __awaiter(this, void 0, void 0, function* () { if (!this.logEvents.length) { return; } // To send logs over the network, we need one of: // // - Ingesting URL and source token // - Custom endpoint // // We fall back to printing to console to avoid network errors in // development environments. if (!config_1.config.isEnvVarsSet()) { this.logEvents.forEach((ev) => (this.config.prettyPrint ? this.config.prettyPrint(ev) : prettyPrint(ev))); this.logEvents = []; return; } const method = 'POST'; const keepalive = true; const body = JSON.stringify(this.logEvents); // clear pending logs this.logEvents = []; const headers = { 'Content-Type': 'application/json', 'User-Agent': 'next-logtail/v' + config_1.Version, }; if (config_1.config.token) { headers['Authorization'] = `Bearer ${config_1.config.token}`; } const reqOptions = { body, method, keepalive, headers }; function sendFallback() { // Do not leak network errors; does not affect the running app return fetch(url, reqOptions).catch(console.error); } try { if (typeof fetch === 'undefined') { const fetch = yield require('whatwg-fetch'); return fetch(url, reqOptions).catch(console.error); } else if (config_1.isBrowser && config_1.isVercel && navigator.sendBeacon) { // sendBeacon fails if message size is greater than 64kb, so // we fall back to fetch. // Navigator has to be bound to ensure it does not error in some browsers // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch try { if (!navigator.sendBeacon.bind(navigator)(url, body)) { return sendFallback(); } } catch (error) { return sendFallback(); } } else { return sendFallback(); } } catch (e) { console.warn(`Failed to send logs to BetterStack: ${e}`); // put the log events back in the queue this.logEvents = [...this.logEvents, JSON.parse(body)]; } }); } } exports.Logger = Logger; exports.log = new Logger({}); const levelColors = { info: { terminal: '32', browser: 'lightgreen', }, debug: { terminal: '36', browser: 'lightblue', }, warn: { terminal: '33', browser: 'yellow', }, error: { terminal: '31', browser: 'red', }, }; function prettyPrint(ev) { const hasFields = Object.keys(ev.fields).length > 0; // check whether pretty print is disabled if (shared_1.isNoPrettyPrint) { let msg = `${ev.level} - ${ev.message}`; if (hasFields) { msg += ' ' + JSON.stringify(ev.fields); } console.log(msg); return; } // print indented message, instead of [object] // We use the %o modifier instead of JSON.stringify because stringify will print the // object as normal text, it loses all the functionality the browser gives for viewing // objects in the console, such as expanding and collapsing the object. let msgString = ''; let args = [ev.level, ev.message]; if (config_1.isBrowser) { msgString = '%c%s - %s'; args = [`color: ${levelColors[ev.level].browser};`, ...args]; } else { msgString = `\x1b[${levelColors[ev.level].terminal}m%s\x1b[0m - %s`; } // we check if the fields object is not empty, otherwise its printed as <empty string> // or just "". if (hasFields) { msgString += ' %o'; args.push(ev.fields); } if (ev.request) { msgString += ' %o'; args.push(ev.request); } console.log.apply(console, [msgString, ...args]); } exports.prettyPrint = prettyPrint; function jsonFriendlyErrorReplacer(key, value) { if (value instanceof Error) { return Object.assign(Object.assign({}, value), { // Explicitly pull Error's non-enumerable properties name: value.name, message: value.message, stack: value.stack }); } return value; } //# sourceMappingURL=logger.js.map