Files
habitica/website/server/libs/logger.js
negue 2535fd7095 Combined Message Pages/Redesign (#15310)
* split component prepare new views / states

* extract empty and disabled state as components

* fix empty state mail icon

* first logic switching between modes, move page to /private-messages/index.vue

* extract autoCompleteHelper.js

* style header + start new message input

* style plus button + focus input

* state logic, types for sanity

* WIP PM new Message started

* add /members/username test

* first design changes to messageCard

* delete private message or chat - based on the mode

* copy as todo

* mention links to modal

* report chat or private message

* WIP likeButton

* likeButton styling

* hide like on private message cards

* fix unit test

* replace copy as todo - to just a copy to clipboard

* style changes

* menu position + like button width

* dropdown items background + like font

* fix like button padding

* move api endpoints and tests around to group inbox methods  + like for inbox private messages

* restyle system messages

* Dropdown Radius and Padding

* WIP system messages

* fix lint

* copy delta commit of allowing liking own private messages

* enable liking private messages

* fix menu non hovered item icon color

* fix import path

* ignore background on system messages

* requested changes + migration

* update migration to update the unique id to some messages and delete the duplicates

* migration based on users pagination

* fix(migration): use Promise.all

* change to bulkWrites per User, and all messages in one run (of a user)

* check for array

* use rest operator ...

* skip sorting to get the users

* remove migration, disable like for private messages without uniqueMessageId

* lean+bulkWrite for likes, add time checks for like and auth for further debugging

* add a limit 2 get the messages by uniqueId

* Adding a simple server start script

* remove pinned nodemon dep

* fix inbox controller/tests

* fix / requested style changes

* fix empty state padding /

* hide avatar weapons on messages - fix avatar spacing on messages

* Hourglass Simplification (#15323)

* begin removing obsolete tests

* begin refactoring

* update cron tests

* cleanup

* finish basic implementation of new logic

* add more subscription tests

* subscription test improvements

* return nextHourglassDate again

* fix gem limit

* fix(test): short circuit this.

* fix(admin): correct logic and style for shrimple subs

* WIP(frontend): draft of main subs page view

* fix hourglass count

* Fix hourglass logic for upgrades

* fix admin panel display

* WIP(subs): extant Stripe state

* fix admin panel strings

* fix missing transaction type

* add new field for cumulative subscription count

* show date for hourglass bonus if it was received

* fix test

* feat(subscription): max Gems progress readout

* fix(css): correct and refactor heights and selection states

* fix(subs): correct border-radius and redirect

* fix(stripe): correct redirect after success

* Admin panel display fixes

* don’t give additional HG for new sub if they already got one this month

* fix issue with promo hourglasses

* fix(subscription): update layout when gifting

* fix(subscriptions): more gift layout revisions

* fix(subscriptions): minor visual updates

* fix(subs): pass autoRenews through Stripe

* fix(subs): gifts DON't renew

* fix(lint): unnecessary ternary

* fix(lint): do negate object ig

* fix(subs): try again on gifts

* fix(subs): unhovery and un-12-monthy

* fix bug with incorrectly giving HG bonus

* remove only

* fix test

* fix test

* fix(subs): also redirect to subs after gift sub

* fix(subs): fix typeError

* fix(g1g1): don't try to find Gems promo during bogo

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <sabe@habitica.com>

* chore(sprites): update subproject

* fix(layout): tighten cancellation note

* fix(subs): Google wording and HG escape

* chore(testing): fake g1g1 dates

* fix(subs): don't hide HG preview entirely

* fix(subs): center next hourglass message

* working validatedTextInput.vue within start-new-conversation-input-header.vue 🎉

* fix(git): remove changes from old develop

* Revert "fix(git): remove changes from old develop"

This reverts commit 0e30f7df00.

* fix(git): no actually just this file i guesss

* adding an empty loading state, hiding

* fought the avatar arch nemesis again

* fix chatMessages (party chat) message spacing

* move disabled text back to above the input area - re-enable input area

* show disabled private messages top panel

* fix font color

* fixing uiStates - removing disabled - moving the own user check to the last

* fix(lint): add missing prop defaults

* fix(lint): object default should be fn

* fix(chat): correct grammar in error

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2025-01-16 16:52:24 -06:00

225 lines
6.7 KiB
JavaScript

// Logger utility
import winston from 'winston';
import { Loggly } from 'winston-loggly-bulk';
import nconf from 'nconf';
import _ from 'lodash';
import {
CustomError,
} from './errors';
const IS_PROD = nconf.get('IS_PROD');
const IS_TEST = nconf.get('IS_TEST');
const ENABLE_LOGS_IN_TEST = nconf.get('ENABLE_CONSOLE_LOGS_IN_TEST') === 'true';
const ENABLE_CONSOLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true';
const LOGGLY_TOKEN = nconf.get('LOGGLY_TOKEN');
const LOGGLY_SUBDOMAIN = nconf.get('LOGGLY_SUBDOMAIN');
const logger = winston.createLogger();
const _config = {
logger,
loggingEnabled: true, // false if no transport has been configured
};
export { _config as _loggerConfig }; // exported for use during tests
if (IS_PROD) {
if (ENABLE_CONSOLE_LOGS_IN_PROD) {
logger
.add(new winston.transports.Console({ // text part
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(
info => `${info.timestamp} - ${info.level} ${info.message}`,
),
),
}))
.add(new winston.transports.Console({ // json part
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
}));
}
if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) {
logger.add(new Loggly({
inputToken: LOGGLY_TOKEN,
subdomain: LOGGLY_SUBDOMAIN,
tags: ['Winston-NodeJS'],
json: true,
}));
}
// Do not log anything when testing unless specified
} else if (!IS_TEST || (IS_TEST && ENABLE_LOGS_IN_TEST)) {
logger
.add(new winston.transports.Console({
level: 'warn', // warn and errors (text part)
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(
info => `${info.timestamp} - ${info.level} ${info.message}`,
),
),
}))
.add(new winston.transports.Console({
level: 'warn', // warn and errors (json part)
format: winston.format.combine(
// Remove stacktrace from json, printed separately
winston.format(info => {
if (info && info.message && typeof info.message.split === 'function') {
const message = info.message.split('\n')[0];
info.message = message;
}
return info;
})(),
winston.format.prettyPrint({
colorize: true,
}),
),
}))
.add(new winston.transports.Console({
level: 'info', // text part
format: winston.format.combine(
// Ignores warn and errors
winston.format(info => {
if (info.level === 'error' || info.level === 'warn') {
return false;
}
return info;
})(),
winston.format.timestamp(),
winston.format.colorize(),
winston.format.printf(info => `${info.timestamp} - ${info.level} ${info.message}`),
),
}))
.add(new winston.transports.Console({
level: 'info', // json part
format: winston.format.combine(
// Ignores warn and errors
winston.format(info => {
if (info.level === 'error' || info.level === 'warn') {
return false;
}
// If there are only two keys (message and level) it means there's nothing
// to print as json
if (Object.keys(info).length <= 2) return false;
return info;
})(),
winston.format.prettyPrint({
colorize: true,
}),
),
}));
} else {
_config.loggingEnabled = false;
}
// exports a public interface insteaf of accessing directly the logger module
const loggerInterface = {
info (...args) {
if (!_config.loggingEnabled) return;
const [_message, _data] = args;
const isMessageString = typeof _message === 'string';
const message = isMessageString ? _message : 'No message provided for log.';
let data;
if (args.length === 1) {
if (isMessageString) {
data = {};
} else {
data = { extraData: _message };
}
} else if (!isMessageString || args.length > 2) {
throw new Error('logger.info accepts up to two arguments: a message and an object with extra data to log.');
} else if (_.isPlainObject(_data)) {
data = _data;
} else {
data = { extraData: _data };
}
logger.info(message, data);
},
// Accepts two argument,
// an Error object (required)
// and an object of additional data to log alongside the error
// If the first argument isn't an Error, it'll call logger.error with all the arguments supplied
error (...args) {
if (!_config.loggingEnabled) return;
const [err, _errorData] = args;
if (args.length > 2) {
throw new Error('logger.error accepts up to two arguments: an error and an object with extra data to log.');
}
let errorData = {};
if (typeof _errorData === 'string') {
errorData = { extraMessage: _errorData };
} else if (_.isPlainObject(_errorData)) {
errorData = _errorData;
} else if (_errorData) {
errorData = { extraData: _errorData };
}
if (err instanceof Error) {
// pass the error stack as the first parameter to logger.error
const stack = err.stack || err.message || err;
if (!errorData.fullError) {
// If the error object has interesting data
// (not only httpCode, message and name from the CustomError class)
// add it to the logs
if (err instanceof CustomError) {
const errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']);
if (Object.keys(errWithoutCommonProps).length > 0) {
errorData.fullError = errWithoutCommonProps;
}
} else {
errorData.fullError = err;
}
}
const loggerArgs = [stack, errorData];
// Treat 4xx errors that are handled as warnings, 5xx and uncaught errors as serious problems
if (!errorData || !errorData.isHandledError || errorData.httpCode >= 500) {
logger.error(...loggerArgs);
} else {
logger.warn(...loggerArgs);
}
} else {
errorData.invalidErr = err;
logger.error('logger.error expects an Error instance', errorData);
}
},
};
// Logs unhandled promises errors
// when no catch is attached to a promise a unhandledRejection event will be triggered
// reason is the error, p the promise where it originated
process.on('unhandledRejection', (reason, promise) => {
loggerInterface.error(reason, { message: 'unhandledPromiseRejection', promise });
});
export function logTime (url, str) {
const now = Date.now();
logger.info(`${url} ${str} started`);
return () => {
logger.info(`${url} ${str} ended: ${Date.now() - now}ms`);
};
}
export default loggerInterface;