// src/utils/loggerUtils.ts
import { getTimestamp } from './getTimestampUtils';
// TODO import { sendErrorLog } from '../services/logServices';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

// Determine the current log level / .environment("/project/.env")
const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

const logger =
{
log: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.log(getTimestamp(), ...args);
}
},
info: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.info(getTimestamp(), ...args);
}
},
warn: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.warn(getTimestamp(), ...args);
}
},
error: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
// Use console.warn instead of console.error to avoid triggering Parcel overlay
console.warn(getTimestamp(), ...args);
}
else
{
// In production, still log errors properly
console.error(getTimestamp(), ...args);
}
},
};

export default logger;

What would be a ideal way to expend on this concept, the logs display fine, expect for I have no idea what file, unless i add into the message. Suggestions
on improvement

Great question! Knowing the origin of a log message (the file and possibly the line number) significantly improves debugging, especially in larger projects.

Here are several approaches and suggestions to enhance your logger utility to include file and line info automatically:
1. Use Error().stack to capture call site info

You can create a helper function that captures the stack trace, parses it, and extracts the caller file and line number. This way, your logger can prepend this info to each log message automatically.

Example:

function getCallerInfo() {
  const stack = new Error().stack || '';
  // Stack trace lines vary by environment and browser
  // For example: at logger.log (...), at Component.tsx:10:5, etc.
  const stackLines = stack.split('\n');

  // The first line is "Error"
  // The second line is this function (getCallerInfo)
  // The third line typically points to the caller of the logger method
  const callerLine = stackLines[3] || stackLines[2] || '';

  // Extract filename:line:column with regex - varies so might need adjustment
  const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [ , file, line, col ] = match;
    // Optional: shorten file path if you want
    const shortFile = file.split('/').slice(-2).join('/');
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
}

Usage inside your logger:

const logger = {
  log: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.log(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  // similarly for info, warn, error...
};

2. Use third-party logging libraries

Libraries like loglevel or winston (for Node.js) offer more complete log management and sometimes source maps integration.

In the frontend React case, loglevel with plugins can help, but capturing callsite info usually requires custom logic similar to above.
3. Use Babel/Webpack source maps & Error stacks (advanced)

When bundled/minified, the file/line info can be lost or obfuscated.

    Source maps decode minified stack traces back to your original code.
    Tools like stacktrace.js can parse and retrieve source mapped stack traces.

This adds complexity but is useful in production if you want reliable source info.
4. Explicitly pass context or label when logging

If automatic callsite detection is too costly or unreliable for your setup, a simple alternative:

logger.log('MyComponent.tsx', 'some message');

or, create small wrappers for each module:

const log = (...args) => logger.log('[MyComponent.tsx]', ...args);

Suggested improvement: Implement getCallerInfo approach:

import { getTimestamp } from './getTimestampUtils';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

function getCallerInfo(): string {
  const stack = new Error().stack || '';
  const stackLines = stack.split('\n');

  // Index might vary based on browsers/env, tweak if needed
  const callerLine = stackLines[3] || stackLines[2] || '';

  const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [ , file, line, col ] = match;
    const shortFile = file.split('/').slice(-2).join('/');
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
}

const logger = {
  log: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.log(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  info: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.info(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  warn: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  error: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
    else {
      console.error(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
};

export default logger;

Summary of best practice:

    Add automatic callsite detection with stack trace parsing for dev logs.
    Retain behavior: errors log properly in prod.
    Optionally truncate and prettify file paths.
    Consider performance: stack trace extraction can be heavy; consider caching or only enable in dev.
    For production, consider sending error logs to remote services with source info.

If you want, I can help you build out a more advanced version too!

so create a private function inside the logger:

function getCallerInfo() {
const stack = new Error().stack || '';
// Stack trace lines vary by environment and browser
// For example: at logger.log (...), at Component.tsx:10:5, etc.
const stackLines = stack.split('\n');

// The first line is "Error"
// The second line is this function (getCallerInfo)
// The third line typically points to the caller of the logger method
const callerLine = stackLines[3] || stackLines[2] || '';

// Extract filename:line:column with regex - varies so might need adjustment
const match = callerLine.match(/((.):(\d+):(\d+))/) || callerLine.match(/at (.):(\d+):(\d+)/);

if (match) {
const [ , file, line, col ] = match;
// Optional: shorten file path if you want
const shortFile = file.split('/').slice(-2).join('/');
return ${shortFile}:${line}:${col};
}

return 'unknown source';
}

thats a good idea

Yes, exactly! Defining getCallerInfo as a private helper function inside your logger module is a clean and effective approach. It encapsulates the logic of extracting call-site info, keeps your logging methods tidy, and allows you to prepend the source location to every log automatically.

Here’s a sample refactor showing how to integrate it into your existing logger:

import { getTimestamp } from './getTimestampUtils';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

// Private helper inside the module
function getCallerInfo(): string {
  const stack = new Error().stack || '';
  const stackLines = stack.split('\n');

  // Adjust the index depending on environment - usually 3rd or 4th line is caller
  const callerLine = stackLines[3] || stackLines[2] || '';

  const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [ , file, line, col ] = match;
    const shortFile = file.split('/').slice(-2).join('/'); // Last 2 path parts
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
}

const logger = {
  log: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.log(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  info: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.info(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  warn: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  error: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      // Avoid triggering Parcel overlay by using warn here
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    } else {
      console.error(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
};

export default logger;

A few notes:

    Because this captures new Error().stack, it might add slight overhead, but typically this is negligible for development logging.
    The regex and stack line index might need minor tweaks based on your precise environment, browser, or bundler.
    You could consider caching getCallerInfo results if you find performance becomes an issue in hot code paths (though usually not necessary).
    You can also extend the logger to include named logger instances per file later for even cleaner usage.

If you like, you can also create a higher-order function or a factory method that returns a logger pre-tied to a file name, but this solution will already give you auto source info in your logs — a great improvement for debugging!

If you want me to help write a fully typed TypeScript version with tests or further enhancements (e.g. controlling verbosity or toggling source info), just ask!

so why are some fucntion called function and others are const?

Great question!

In JavaScript and TypeScript, both function declarations and const-assigned arrow functions are valid ways to define functions, but they have some subtle differences in behavior and usage patterns. Understanding these differences can help you decide when to use each.
1. Function Declarations:

function getCallerInfo() {
  // ...
}

    Hoisting: Function declarations are hoisted, meaning the entire function is loaded into memory at the start of execution. You can call the function anywhere in the same scope, even before its declaration in the source code.

    sayHello(); // Works fine

    function sayHello() {
      console.log('Hello!');
    }

    Named function: The function has a name (getCallerInfo), useful for stack traces and debugging.

    Typically used for normal functions that don't require closures or reassignment.

2. Function Expressions (const + arrow function or regular function assigned to a const):

const getCallerInfo = () => {
  // ...
};

or

const getCallerInfo = function() {
  // ...
};

    Not hoisted: Only the variable declaration (const getCallerInfo) is hoisted (but uninitialized), so the function itself is available only after this line executes. Calling it before will cause a runtime error.

    Block-scoped: Because it’s declared with const (or let), it's block-scoped, meaning it's limited to the block {} where it's declared.

    Arrow functions do not have their own this, arguments, super, or new.target, which makes them useful for closures or callbacks where you want to preserve the lexical this.

    Commonly used when you want to assign functions to variables, pass them around as values, or keep them inside a block scope.

When to prefer one over the other?
Aspect 	Function Declaration 	Const with Arrow Function
Hoisting 	Yes 	No
this binding 	Own (dynamic) 	Lexical (inherited)
Can be anonymous? 	No, must have a name 	Can be anonymous
Useful for helper/private functions inside modules? 	Yes 	Yes (often to keep scope)
Can be reassigned? 	No 	No (if const), yes if let
Readability 	Classic and clear 	Concise, modern syntax
In your logger example:

    You can put getCallerInfo as a function declaration because you want it scoped to the module and usable inside the logger methods.
    You could also write it as a const arrow function, which might be preferable if you want lexical this or prefer the modern syntax:

const getCallerInfo = () => { /*...*/ };

In this case, getCallerInfo doesn’t use this, so the difference is minimal.
Summary

    Use function declaration if you want hoisting or a classic named function.
    Use const + arrow function if you want to avoid hoisting, want lexical this, or prefer consistent ES6 syntax.

For private helper functions inside a module, both are perfectly fine — it comes down mostly to style preferences and minor scoping considerations. Many codebases prefer arrow functions assigned to const for consistency, especially when mixing with React functional components.

If you want, I can help rewrite your logger with all helper functions as arrow functions for consistency!

i want to follow modern coding for react

Great! If you want to follow modern React/TypeScript coding style, the preferred approach is to use arrow functions assigned to const for helper functions within modules.

This aligns well with modern ES6+ syntax and patterns used in React functional components.

Here’s how you can refactor getCallerInfo and the logger accordingly:

import { getTimestamp } from './getTimestampUtils';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

// Use arrow function assigned to const - modern style
const getCallerInfo = (): string => {
  const stack = new Error().stack || '';
  const stackLines = stack.split('\n');

  // Typically 3rd or 4th line in call stack is the caller info
  const callerLine = stackLines[3] || stackLines[2] || '';

  const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [ , file, line, col ] = match;
    const shortFile = file.split('/').slice(-2).join('/');
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
};

const logger = {
  log: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.log(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  info: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.info(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  warn: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
  error: (...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      // Avoid triggering Parcel overlay by using warn here
      console.warn(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    } else {
      console.error(getTimestamp(), `[${getCallerInfo()}]`, ...args);
    }
  },
};

export default logger;

Why this is modern best practice:

    Using const with arrow functions ensures block scoping and avoids hoisting confusion.
    Arrow functions have lexical this, which often aligns better with how React components and hooks work.
    Consistent arrow function usage improves readability and maintainability, especially in teams using React hooks and functional components.
    Avoids surprises you sometimes get with function hoisting or dynamic this binding.

Bonus tips:

    For even better type safety and maintainability, consider typing your logger methods explicitly:

type LoggerFn = (...args: any[]) => void;

const logger: {
  log: LoggerFn;
  info: LoggerFn;
  warn: LoggerFn;
  error: LoggerFn;
} = { /*...*/ }

Or export a typed interface if you customize it.

Let me know if you want me to generate a fully typed TypeScript version, or if you want tips on adding contextual loggers per file/module!

const getCallerInfo = (): string => {
const stack = new Error().stack || '';
const stackLines = stack.split('\n');

// Typically 3rd or 4th line in call stack is the caller info
const callerLine = stackLines[3] || stackLines[2] || '';

const match = callerLine.match(/((.):(\d+):(\d+))/) || callerLine.match(/at (.):(\d+):(\d+)/);

if (match) {
const [ , file, line, col ] = match;
const shortFile = file.split('/').slice(-2).join('/');
return ${shortFile}:${line}:${col};
}

return 'unknown source';
};

what if im getting mostly unknown? should I add a option to add the filename? then arg?

Good observation! If you’re often getting "unknown source", it means the stack parsing isn’t reliably finding the file/line info for your environment or bundler setup.

This can happen for many reasons:

    Source maps aren’t enabled or accessible (especially in production).
    Stack trace formats differ among browsers.
    The function call stack might be shorter or optimized out (in dev tools or certain transpilation modes).
    Bundlers or runtimes sometimes mangle filenames or remove stack info to minimize size.

How to handle this gracefully?
Option A: Add an optional filename/context string argument to the logger calls

Let your logger API allow you to provide an explicit context (file/module name), which will be used when getCallerInfo() fails or all the time if you prefer.

Example:

type LoggerFn = (contextOrMessage?: string | any, ...args: any[]) => void;

Usage:

logger.log('MyComponent.tsx', 'Some message');

Implementation idea

Refactor your logger methods to detect if the first arg is a string (context) or not. If it’s a string, treat it as context and include it; otherwise fallback to getCallerInfo().

Example:

const getCallerOrContext = (maybeContext?: string): string => {
  if (maybeContext && typeof maybeContext === 'string') {
    return maybeContext;
  }
  return getCallerInfo();
};

const logger = {
  log: (maybeContextOrMessage?: any, ...args: any[]) => {
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      const context = getCallerOrContext(maybeContextOrMessage);
      // If context was given, treat maybeContextOrMessage as context and args as messages
      // else treat maybeContextOrMessage as first message
      if (typeof maybeContextOrMessage === 'string') {
        console.log(getTimestamp(), `[${context}]`, ...args);
      } else {
        console.log(getTimestamp(), `[${context}]`, maybeContextOrMessage, ...args);
      }
    }
  },
  // similarly for info, warn, error...
};

This way:

    You can call logger.log('MyFile.tsx', 'Some message') and explicitly inject context.
    Or just call logger.log('Some message') and context will be auto-detected from stack.
    If auto-detection fails, you can provide context manually.

Option B: Accept a logger instance factory per file

Another approach is to export a function that returns a logger bound to a given context:

function createLogger(context?: string) {
  return {
    log: (...args: any[]) => {
      const ctx = context || getCallerInfo();
      if (LOG_LEVEL === LOG_LEVELS.DEVE) {
        console.log(getTimestamp(), `[${ctx}]`, ...args);
      }
    },
    // etc.
  };
}

Usage in your module:

const log = createLogger('MyComponent.tsx');
log.log('Message here');

This eliminates repeated passing of context strings and is more ergonomic.
Summary

    If you get many 'unknown source', it’s practical to allow explicit context override.
    Either add an optional first-argument context string to your logging calls, or use per-file/context logger instances.
    This approach balances convenience and reliability without depending fully on brittle stack-trace parsing.
    Stack trace parsing remains a nice-to-have fallback for cases where you don’t want to specify context manually.

If you want, I can help implement either option in your code! Just let me know which style you’d prefer.

but would that mean i always have to add the file?

Good point — no, ideally you shouldn’t always have to add the file name manually.

The goal is to:

    Automatically include call-site info (file/line) when it can be reliably extracted, and
    Allow manual override only when automatic detection fails or the info is missing/unhelpful.

How to achieve that with minimal manual work?

Make the manual file/context argument optional — your logger tries getCallerInfo() first and uses it if it’s not 'unknown source'. Only if auto detection returns 'unknown source' does it fallback to the optional user-provided override (or a default string).
Here’s a pattern for that:

const getCallerInfo = (): string => {
  // ... your existing code to parse stack, returns string or 'unknown source'
};

const logger = {
  log: (maybeContextOrMessage?: any, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

    let context: string;
    let messages: any[];

    if (typeof maybeContextOrMessage === 'string') {
      const autoContext = getCallerInfo();
      // Use automatic context if available and not unknown, else use manual provided arg as context
      context = autoContext !== 'unknown source' ? autoContext : maybeContextOrMessage;
      messages = args;
      
      // If context equals manual argument and context is auto (better), 
      // we want to actually print the manual argument as message too
      if (context === maybeContextOrMessage && autoContext !== 'unknown source') {
        // Put manual argument back into the message list  
        messages = [maybeContextOrMessage, ...args];
      }
    } else {
      context = getCallerInfo();
      messages = maybeContextOrMessage !== undefined ? [maybeContextOrMessage, ...args] : args;
    }

    console.log(getTimestamp(), `[${context}]`, ...messages);
  },

  // Similarly for info, warn, error...
};

How this works:

    If you call:

logger.log('MyComponent.tsx', 'Some message');

and auto-detection returns 'unknown source', it will use 'MyComponent.tsx' as context, and log 'Some message'.

If you call:

logger.log('Some message');

and auto-detection works, it uses that info as context and logs 'Some message'.

If you call:

    logger.log('MyComponent.tsx');

    and auto-detection returns a real file:line, it’ll use auto detection and include 'MyComponent.tsx' as part of the message so nothing is lost.

Summary

    You rarely have to provide the file name manually.
    When the stack parsing fails (which can happen in some browsers/configs), manual context is a fallback.
    If you provide manual context but auto detection works, your manual context becomes a message instead, so you don’t lose info.
    This keeps your logger ergonomic, auto-magical in most cases, but flexible when needed.

If you'd like, I can prepare a full working example of this approach to paste into your logger — let me know!

how does it know the difference?

Great question!

The key is how the logger function checks the type of the first argument and uses heuristics to decide whether it's:

    A context string (the file or source info you optionally provide explicitly), or
    The first log message itself.

How it differentiates:

    Check if the first argument is a string:

if (typeof maybeContextOrMessage === 'string') {
  // Could be context or message
}
else {
  // Definitely not a context string, so treat it as first message
}

    If it’s a string, the function:

    Calls getCallerInfo(), which tries to auto-detect the call site info.

    If getCallerInfo() returns something useful (not 'unknown source'), the logger assumes the auto-detected info is better, so it treats the first argument as a message part, not as context.

    If getCallerInfo() returns 'unknown source', it means auto-detection failed, so the function believes the first argument is the context (filename or identifier), and subsequent arguments are messages.

    If the first argument is not a string, then treat it as the first message, and always use the auto-detected context.

Quick example to illustrate logic:

const autoContext = getCallerInfo();

if (typeof maybeContextOrMessage === 'string') {
  if (autoContext !== 'unknown source') {
    // Good auto detection, use it!
    context = autoContext;
    messages = [maybeContextOrMessage, ...args]; // first arg is message
  } else {
    // Auto detection failed, so first arg = manual context
    context = maybeContextOrMessage;
    messages = args; // rest are messages
  }
}
else {
  // first arg is a message or undefined
  context = autoContext;
  messages = maybeContextOrMessage !== undefined ? [maybeContextOrMessage, ...args] : args;
}

Summary

    If the first argument is a string and auto-detection is successful, your string is part of the message.
    If the first argument is a string but auto-detection failed, your string is treated as context.
    If the first argument is anything else (object, number, undefined...), it’s a message.
    The context always comes from auto-detection if possible, else manual override.

This approach:

    Avoids you having to always manually pass the file name.
    Provides a fallback to manual context string only when stack trace info isn’t available.
    Keeps your API neat and flexible.

If you want, I can provide a clean full TypeScript example of this logger function with explanatory comments!

but the args are strings and so is the filename

Great point! Both the filename/context you might want to pass and the log message strings are just strings. So how does the logger reliably tell them apart?
The key is not trying to guess every time — but using the presence or absence of a manual param and the success of getCallerInfo() to decide which string is which.
Why this works in practice:

    If you provide a string and getCallerInfo() returns 'unknown source', it's very likely that first string is a context/file name.
    If you provide a string and getCallerInfo() returns a real source (file + line), your first string is almost certainly a message, because the logger already knows where the call came from.
    Otherwise (if first arg not string), it’s definitely a message.

It's a heuristic — no foolproof way if you only rely on argument types. But this approach balances convenience and flexibility in real projects.
To put simply
Condition 	Treat first string arg as:
typeof firstArg === "string" and getCallerInfo() === "unknown source" 	Manual context (filename)
typeof firstArg === "string" and getCallerInfo() is valid file:line 	Log message
typeof firstArg !== "string" 	Log message
Example use cases

// Auto detection works, so first string is a message
logger.log('Hello World'); 
// logs [MyComponent.tsx:42:10] Hello World

// Auto detection fails (rarely), so first string is manual context & next arg is message
logger.log('MyComp.tsx', 'Hello World'); 
// logs [MyComp.tsx] Hello World (because getCallerInfo returned "unknown source")

// If first argument is e.g. an object, treat it as a message
logger.log({ some: 'data' }); 
// logs [MyComponent.tsx:42:10] { some: 'data' }

Summary

Yes, both manual context and messages are strings, but since you only supply manual context when auto detection fails, the logger can disambiguate based on the auto detection result.

If you always provide a filename as the first arg regardless of detection, the logger will treat that string as a message if getCallerInfo() worked.

This is a simple, practical way to handle string ambiguity without complex API changes or separate method signatures.

If you want, I can help you implement a strict API (for example, requiring an options object for context) — but this heuristic works nicely in most dev scenarios!

// src/utils/getTimestamp.ts

export const getTimestamp = (): string =>
{
const now = new Date();

// ISO format for consistency
return `(${now.toISOString()})`;

};

// src/types/logLevelUtils.ts

/**

    These constants help categorize log messages based on their severity and purpose,
    facilitating effective logging management and monitoring.
    Using 'as const' makes the object immutable, preventing any accidental modifications.
    This ensures type safety and allows TypeScript to infer literal types instead of widening them.
    */
    export const LOG_LEVELS =
    {
    DEVE: 'deve',// Development mode logging. Enables verbose logging to the console for debugging purposes.
    PROD: 'prod',// Production mode logging. Reduces console output, focusing instead on essential logs and errors.
    INFO: 'info',
    WARN: 'warn',
    ERROR: 'error',
    } as const;// as const makes it read-only

// LogLevel type is defined as the keys of LOG_LEVELS, restricting it to 'DEVE', 'PROD', 'INFO', 'WARN', 'ERROR'.
export type LogLevel = keyof typeof LOG_LEVELS;

// src/utils/loggerUtils.ts
import { getTimestamp } from './getTimestampUtils';
// TODO import { sendErrorLog } from '../services/logServices';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

// Determine the current log level / .environment("/project/.env")
const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

/**

    Get Caller Information
    */
    const getCallerInfo = (): string =>
    {
    const stack = new Error().stack || '';
    const stackLines = stack.split('\n');

    // Typically 3rd or 4th line in call stack is the caller info
    const callerLine = stackLines[3] || stackLines[2] || '';
    const match = callerLine.match(/((.):(\d+):(\d+))/) || callerLine.match(/at (.):(\d+):(\d+)/);

    if (match)
    {
    const [ , file, line, col ] = match;
    const shortFile = file.split('/').slice(-2).join('/');

    return ${shortFile}:${line}:${col};
    }

    return 'unknown source';

};

const logger =
{
log: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.log(getTimestamp(), ...args);
}
},
info: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.info(getTimestamp(), [${getCallerInfo()}], ...args);
}
},
warn: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.warn(getTimestamp(), [${getCallerInfo()}], ...args);
}
},
error: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
// Use console.warn instead of console.error to avoid triggering Parcel overlay
console.warn(getTimestamp(), [${getCallerInfo()}], ...args);
}
else
{
// In production, still log errors properly
console.error(getTimestamp(), [${getCallerInfo()}], ...args);
}
},
};

export default logger;

help refactor my logger to handle filename and then args, filename can be empty. Use typescript aswell

Certainly! To refactor your logger so it optionally accepts a filename/context as the first argument followed by the message arguments — and falls back to automatic caller info detection if filename is empty or not provided — here's a clean, fully typed TypeScript version.
What this refactor will do:

    The first argument may be a string representing the filename/context, or omitted/empty.
    The rest are the log message arguments.
    If filename/context is not provided or empty string, use getCallerInfo() to auto-detect call source.
    Keep existing log level behavior.
    Use proper typing for logger methods.

Refactored code for loggerUtils.ts

import { getTimestamp } from './getTimestamp'; // fixed import to match your path
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

const getCallerInfo = (): string => {
  const stack = new Error().stack || '';
  const stackLines = stack.split('\n');

  // Usually 3rd or 4th line is the caller
  const callerLine = stackLines[3] || stackLines[2] || '';
  const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [, file, line, col] = match;
    const shortFile = file.split('/').slice(-2).join('/');
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
};

// Type for logger method: first arg optional string filename, then rest log args
type LogMethod = (filenameOrMessage?: string, ...args: any[]) => void;

const logger = {
  log: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      // first arg is context/filename
      context = filenameOrMessage;
      messages = args;
    } else {
      // first arg is message OR undefined
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    // Display context only if available
    if (context) {
      console.log(getTimestamp(), `[${context}]`, ...messages);
    } else {
      console.log(getTimestamp(), ...messages);
    }
  }) as LogMethod,

  info: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    if (context) {
      console.info(getTimestamp(), `[${context}]`, ...messages);
    } else {
      console.info(getTimestamp(), ...messages);
    }
  }) as LogMethod,

  warn: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    if (context) {
      console.warn(getTimestamp(), `[${context}]`, ...messages);
    } else {
      console.warn(getTimestamp(), ...messages);
    }
  }) as LogMethod,

  error: ((filenameOrMessage?: string, ...args: any[]) => {
    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    // For dev, use console.warn to avoid Parcel overlay
    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      if (context) {
        console.warn(getTimestamp(), `[${context}]`, ...messages);
      } else {
        console.warn(getTimestamp(), ...messages);
      }
    } else {
      // In prod, always use console.error
      if (context) {
        console.error(getTimestamp(), `[${context}]`, ...messages);
      } else {
        console.error(getTimestamp(), ...messages);
      }
    }
  }) as LogMethod,
};

export default logger;

How to use it:

import logger from './loggerUtils';

// Auto detect caller info
logger.log('Some log message', { some: 'data' });

// Manually provide context filename or label
logger.info('MyComponent.tsx', 'This is an info message');
logger.warn('MyComponent.tsx', 'Watch out for this!');

// Without message (just context + default message)
logger.error('MyComponent.tsx');

Summary

    The logger methods all accept an optional first argument string, treated as a file/context.
    If not given or empty string, fallback to automatically detecting caller with getCallerInfo().
    Messages are the rest of the arguments.
    The method signature and implementation are fully typed with LogMethod.
    The logger respects your LOG_LEVEL variable and behaves differently in dev/prod modes, as before.

If you want me to help you DRY this with a helper function wrapping the common logic, just say so!

import { getTimestamp } from './getTimestamp'; // fixed import to match your path
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

const getCallerInfo = (): string => {
const stack = new Error().stack || '';
const stackLines = stack.split('\n');

// Usually 3rd or 4th line is the caller
const callerLine = stackLines[3] || stackLines[2] || '';
const match = callerLine.match(/((.):(\d+):(\d+))/) || callerLine.match(/at (.):(\d+):(\d+)/);

if (match) {
const [, file, line, col] = match;
const shortFile = file.split('/').slice(-2).join('/');
return ${shortFile}:${line}:${col};
}

return 'unknown source';
};

// Type for logger method: first arg optional string filename, then rest log args
type LogMethod = (filenameOrMessage?: string, ...args: any[]) => void;

const logger = {
log: ((filenameOrMessage?: string, ...args: any[]) => {
if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

let context = '';
let messages: any[] = [];

if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
  // first arg is context/filename
  context = filenameOrMessage;
  messages = args;
} else {
  // first arg is message OR undefined
  context = getCallerInfo();
  messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
}

// Display context only if available
if (context) {
  console.log(getTimestamp(), `[${context}]`, ...messages);
} else {
  console.log(getTimestamp(), ...messages);
}

}) as LogMethod,

info: ((filenameOrMessage?: string, ...args: any[]) => {
if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

let context = '';
let messages: any[] = [];

if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
  context = filenameOrMessage;
  messages = args;
} else {
  context = getCallerInfo();
  messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
}

if (context) {
  console.info(getTimestamp(), `[${context}]`, ...messages);
} else {
  console.info(getTimestamp(), ...messages);
}

}) as LogMethod,

warn: ((filenameOrMessage?: string, ...args: any[]) => {
if (LOG_LEVEL !== LOG_LEVELS.DEVE) return;

let context = '';
let messages: any[] = [];

if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
  context = filenameOrMessage;
  messages = args;
} else {
  context = getCallerInfo();
  messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
}

if (context) {
  console.warn(getTimestamp(), `[${context}]`, ...messages);
} else {
  console.warn(getTimestamp(), ...messages);
}

}) as LogMethod,

error: ((filenameOrMessage?: string, ...args: any[]) => {
let context = '';
let messages: any[] = [];

if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
  context = filenameOrMessage;
  messages = args;
} else {
  context = getCallerInfo();
  messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
}

// For dev, use console.warn to avoid Parcel overlay
if (LOG_LEVEL === LOG_LEVELS.DEVE) {
  if (context) {
    console.warn(getTimestamp(), `[${context}]`, ...messages);
  } else {
    console.warn(getTimestamp(), ...messages);
  }
} else {
  // In prod, always use console.error
  if (context) {
    console.error(getTimestamp(), `[${context}]`, ...messages);
  } else {
    console.error(getTimestamp(), ...messages);
  }
}

}) as LogMethod,
};

export default logger;

Can we also add this into the above code:

const safeStringifyArg = (arg: any): string =>
{
if (arg instanceof Error)
{
return arg.stack || arg.message || String(arg);
}
if (typeof arg === 'object' && arg !== null)
{
try
{
return JSON.stringify(arg);
}
catch
{
return '[object Object]';
}
}

return String(arg);

};

const logger =
{
log: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.log(getTimestamp(), ...args);
}
else if (LOG_LEVEL === LOG_LEVELS.PROD)
{
storeLog(LOG_LEVELS.INFO, args.map(safeStringifyArg).join(' '));
}
},
info: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.info(getTimestamp(), [${getCallerInfo()}], ...args);
}
else if (LOG_LEVEL === LOG_LEVELS.PROD)
{
storeLog(LOG_LEVELS.INFO, args.map(safeStringifyArg).join(' '));
}
},
warn: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
console.warn(getTimestamp(), [${getCallerInfo()}], ...args);
}
else if (LOG_LEVEL === LOG_LEVELS.PROD)
{
storeLog(LOG_LEVELS.WARN, args.map(safeStringifyArg).join(' '));
}
},
error: (...args: any[]) =>
{
if (LOG_LEVEL === LOG_LEVELS.DEVE)
{
// Use console.warn instead of console.error to avoid triggering Parcel overlay
console.warn(getTimestamp(), [${getCallerInfo()}], ...args);
}
else
{
// In production, still log errors properly
console.error(getTimestamp(), [${getCallerInfo()}], ...args);

        // Safely stringify all error args for storage or sending
        const message = args.map(safeStringifyArg).join(' ');
        
        if (LOG_LEVEL === LOG_LEVELS.PROD)
        {
            storeLog(LOG_LEVELS.ERROR, message);
        }
        
        // TODO Send the error log immediately even in production
        sendErrorLog(LOG_LEVELS.ERROR, message);
    }
},

};

/**

    Send stored logs to server when the user comes back online.

    Clears stored logs after successful send.
    */
    const sendStoredLogs = async () =>
    {
    const storedLogs = JSON.parse(localStorage.getItem('storedLogs') || '[]');

    for (const log of storedLogs)
    {
    await sendErrorLog(log.level, log.message);
    }

    // Clear logs from local storage after sending
    localStorage.removeItem('storedLogs');

};

// TODO Add a listener for when the user comes back online
window.addEventListener('online', sendStoredLogs);

Certainly! You want to combine:

    your existing typed logger with optional filename/context + automatic callsite detection, AND
    the safeStringifyArg logic that:
        stringifies log args safely (handles errors and circular objects),
        stores logs in production mode (using the hypothetical storeLog function),
        invokes the sendErrorLog service immediately on errors, and
        adds offline log sending with a listener on "online" event.

Here's a fully integrated, cleaned, and typed TypeScript implementation of your logger that merges all these aspects:

import { getTimestamp } from './getTimestamp';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';

// Placeholder: you have to implement or import these in your project
declare function storeLog(level: typeof LOG_LEVELS[keyof typeof LOG_LEVELS], message: string): void;
declare function sendErrorLog(level: typeof LOG_LEVELS[keyof typeof LOG_LEVELS], message: string): void;

const LOG_LEVEL: LogLevel = (process.env.REACT_APP_LOG_LEVEL as LogLevel) || LOG_LEVELS.PROD;

const getCallerInfo = (): string => {
  const stack = new Error().stack || '';
  const stackLines = stack.split('\n');

  const callerLine = stackLines[3] || stackLines[2] || '';
  const match =
    callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);

  if (match) {
    const [, file, line, col] = match;
    const shortFile = file.split('/').slice(-2).join('/');
    return `${shortFile}:${line}:${col}`;
  }

  return 'unknown source';
};

const safeStringifyArg = (arg: any): string => {
  if (arg instanceof Error) {
    return arg.stack || arg.message || String(arg);
  }
  if (typeof arg === 'object' && arg !== null) {
    try {
      return JSON.stringify(arg);
    } catch {
      return '[object Object]';
    }
  }
  return String(arg);
};

type LogMethod = (filenameOrMessage?: string, ...args: any[]) => void;

const formatLog = (context: string, messages: any[]): [string, any[]] => {
  return context ? [`[${context}]`, messages] : ['', messages];
};

const logger = {
  log: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE && LOG_LEVEL !== LOG_LEVELS.PROD) return;

    let context = '';
    let messages: any[] = [];

    // Determine context and messages
    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      const [prefix, msgs] = formatLog(context, messages);
      if (prefix) {
        console.log(getTimestamp(), prefix, ...msgs);
      } else {
        console.log(getTimestamp(), ...msgs);
      }
    } else if (LOG_LEVEL === LOG_LEVELS.PROD) {
      const messageStr = messages.map(safeStringifyArg).join(' ');
      storeLog(LOG_LEVELS.INFO, messageStr);
    }
  }) as LogMethod,

  info: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE && LOG_LEVEL !== LOG_LEVELS.PROD) return;

    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      const [prefix, msgs] = formatLog(context, messages);
      if (prefix) {
        console.info(getTimestamp(), prefix, ...msgs);
      } else {
        console.info(getTimestamp(), ...msgs);
      }
    } else if (LOG_LEVEL === LOG_LEVELS.PROD) {
      const messageStr = messages.map(safeStringifyArg).join(' ');
      storeLog(LOG_LEVELS.INFO, messageStr);
    }
  }) as LogMethod,

  warn: ((filenameOrMessage?: string, ...args: any[]) => {
    if (LOG_LEVEL !== LOG_LEVELS.DEVE && LOG_LEVEL !== LOG_LEVELS.PROD) return;

    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      const [prefix, msgs] = formatLog(context, messages);
      if (prefix) {
        console.warn(getTimestamp(), prefix, ...msgs);
      } else {
        console.warn(getTimestamp(), ...msgs);
      }
    } else if (LOG_LEVEL === LOG_LEVELS.PROD) {
      const messageStr = messages.map(safeStringifyArg).join(' ');
      storeLog(LOG_LEVELS.WARN, messageStr);
    }
  }) as LogMethod,

  error: ((filenameOrMessage?: string, ...args: any[]) => {
    let context = '';
    let messages: any[] = [];

    if (typeof filenameOrMessage === 'string' && filenameOrMessage.trim() !== '') {
      context = filenameOrMessage;
      messages = args;
    } else {
      context = getCallerInfo();
      messages = filenameOrMessage !== undefined ? [filenameOrMessage, ...args] : args;
    }

    const [prefix, msgs] = formatLog(context, messages);
    const messageStr = messages.map(safeStringifyArg).join(' ');

    if (LOG_LEVEL === LOG_LEVELS.DEVE) {
      if (prefix) {
        console.warn(getTimestamp(), prefix, ...msgs);
      } else {
        console.warn(getTimestamp(), ...msgs);
      }
    } else {
      if (prefix) {
        console.error(getTimestamp(), prefix, ...msgs);
      } else {
        console.error(getTimestamp(), ...msgs);
      }
      storeLog(LOG_LEVELS.ERROR, messageStr);
      sendErrorLog(LOG_LEVELS.ERROR, messageStr);
    }
  }) as LogMethod,
};

/**
 * Send stored logs to server when the user comes back online.
 * Clears stored logs after successful send.
 */
const sendStoredLogs = async () => {
  try {
    const storedLogsStr = localStorage.getItem('storedLogs') || '[]';
    const storedLogs: { level: string; message: string }[] = JSON.parse(storedLogsStr);

    for (const log of storedLogs) {
      await sendErrorLog(log.level, log.message);
    }
    localStorage.removeItem('storedLogs');
  } catch (error) {
    // Optionally handle parsing or network errors here
    console.error('Failed to send stored logs:', error);
  }
};

// Listen for user coming back online to flush stored logs
window.addEventListener('online', sendStoredLogs);

export default logger;

Key Highlights:

    safeStringifyArg handles Errors and circular JSON gracefully.
    Logs in DEV mode use console.[log|info|warn|error] and include caller info or manual context.
    Logs in PROD stringify arguments, call storeLog with appropriate levels, and send error logs immediately.
    The external functions storeLog and sendErrorLog are declared as placeholders and must be implemented in your services.
    Added error handling in sendStoredLogs for safety.
    Attached the "online" event listener to flush stored logs when connection returns.

If you want, I can help you build storeLog and sendErrorLog implementations or provide further test coverage too. Let me know!

// Placeholder: you have to implement or import these in your project
declare function storeLog(level: typeof LOG_LEVELS[keyof typeof LOG_LEVELS], message: string): void;
declare function sendErrorLog(level: typeof LOG_LEVELS[keyof typeof LOG_LEVELS], message: string): void;

I dont get this?

// src/services/logServices.tsx
import axios, { AxiosResponse } from 'axios';
import { LogLevel } from '../utils/logLevelUtils';

// Define the URL from environment variables
const API_URL = process.env.REACT_APP_API_URL;

// Define the structure of the log data
interface LogData
{
level: LogLevel;
message: string;
}

// @param {string} level - The level of the log
const sendErrorLog = async (level: LogLevel, ...args: any[]): Promise

try
{
    const response: AxiosResponse = await axios.post<LogData>(`${API_URL}/api/log/error`, {
        level,
        message
    },{
         withCredentials: true// Include cookies if needed
    });
    
    if (response.status === 200)
    {
        console.log('Log sent successfully:', response.data);
    }
    else
    {
        throw new Error("Failed to send error log.");
    }
}
catch (error)
{
    // Handle the error (for example, logging the error or storing it locally)
    console.error('Failed to send log to server:', error);
}

};

export { sendErrorLog };

Great question! The lines with declare function storeLog(...) and declare function sendErrorLog(...) are TypeScript ambient declarations used in the logger file to tell the compiler:

    “Hey, these functions exist somewhere else, trust me. I am not implementing them here.”

Why you see declare function?

Because in your logger file:

    You don’t want to implement these functions (storeLog, sendErrorLog) inside loggerUtils.ts.
    But you want to call them in your logger for sending/storing logs.
    So, declare tells TypeScript those exist and have these types, preventing errors.
    The actual implementations need to live elsewhere, for example in src/services/logServices.tsx.

How to connect your service (sendErrorLog) to the logger?

    Remove the declare function sendErrorLog in loggerUtils.ts.

    Import sendErrorLog from your logServices.tsx file (and similarly storeLog if you implement it).

Example:

// src/utils/loggerUtils.ts
import { sendErrorLog } from '../services/logServices';
import { storeLog } from '../services/logServices'; // implement storeLog similarly

// now use sendErrorLog() and storeLog() as regular imported functions

    Implement storeLog yourself. (Your snippet only shows sendErrorLog.)

Example simple storeLog implementation that saves logs in localStorage:

// src/services/logServices.tsx
import { LogLevel } from '../utils/logLevelUtils';

interface StoredLog {
  level: LogLevel;
  message: string;
  timestamp: string;
}

const storeLog = (level: LogLevel, message: string) => {
  try {
    const existingLogsRaw = localStorage.getItem('storedLogs') || '[]';
    const existingLogs: StoredLog[] = JSON.parse(existingLogsRaw);

    existingLogs.push({ level, message, timestamp: new Date().toISOString() });

    localStorage.setItem('storedLogs', JSON.stringify(existingLogs));
  } catch (error) {
    console.error('Failed to store log locally:', error);
  }
};

export { storeLog, sendErrorLog }; // export both now

Summary

    declare function is a way to say "this function exists but is implemented elsewhere" — a placeholder for the compiler.
    Replace declare with actual imports from your implementation files.
    You already have sendErrorLog implemented — import and use it.
    Implement and import storeLog similarly, or adjust logger code to your needs.

Final example import section in your logger

import { getTimestamp } from './getTimestamp';
import { LOG_LEVELS, LogLevel } from './logLevelUtils';
import { sendErrorLog, storeLog } from '../services/logServices'; // correct relative path

If you'd like, I can help you modify your logger and service files to the final working version with these imports!

Is your storeLog file a refactored version of my:

const sendStoredLogs = async () =>
{
	const storedLogs = JSON.parse(localStorage.getItem('storedLogs') || '[]');
	
	for (const log of storedLogs)
	{
		await sendErrorLog(log.level, log.message);
	}
	
	// Clear logs from local storage after sending
	localStorage.removeItem('storedLogs');
};

That is meant to be inside the logger file? the only outside file is the logServices file that is used to make the api call.