Skip to content

Commit

Permalink
[2.2.1] (#358)
Browse files Browse the repository at this point in the history
fix: Resolve "_a.substring is not a function" error with applicationInsights

Issue was found in applicationinsights Library's TelemetryClient.trackTrace and EnvelopeFactory.createTraceData methods. This fix targets the compatibility issue observed in version 2.5.1 or higher.
Co-authored-by: Florian Rudisch <[email protected]>
  • Loading branch information
maaaNu and frudisch authored Nov 1, 2023
1 parent dbd8cd5 commit 54c991f
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 70 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CHANGELOG

## 2.2.1 (31.10.2023)
~ Resolve "_a.substring is not a function" error with applicationInsights. Issue was found in applicationinsights Library's TelemetryClient.trackTrace and EnvelopeFactory.createTraceData methods. This fix targets the compatibility issue observed in version 2.5.1 or higher.

## 2.2.0 (31.10.2023)
+ Introduced responseValidation functionality in the middleware. This new feature enhances the robustness of your applications by enabling schema validation for handlers' responses.
+ Implemented the capability to dynamically enable or disable middleware functions within the execution flow. This addition brings conditional processing to your middleware stack, allowing greater control based on runtime conditions or application logic. Functions can now be seamlessly included or excluded from the execution process by resolving to true or false through a new integration pattern. This feature ensures that your application maintains high efficiency and adaptability in handling requests and processing logic.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@senacor/azure-function-middleware",
"version": "2.2.0",
"version": "2.2.1",
"description": "Middleware for azure functions to handle authentication, authorization, error handling and logging",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
42 changes: 22 additions & 20 deletions src/appInsights/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,62 @@ import { Logger } from '@azure/functions';
import { TelemetryClient } from 'applicationinsights';
import { SeverityLevel } from 'applicationinsights/out/Declarations/Contracts';

const consoleDefaultLog = (message: string): void => {
console.log(message);
import { stringify } from '../util/stringify';

const consoleDefaultLog = (...args: unknown[]): void => {
console.log(args);
};

consoleDefaultLog.error = (message: string): void => {
console.error(message);
consoleDefaultLog.error = (...args: unknown[]): void => {
console.error(args);
};

consoleDefaultLog.warn = (message: string): void => {
console.warn(message);
consoleDefaultLog.warn = (...args: unknown[]): void => {
console.warn(args);
};

consoleDefaultLog.info = (message: string): void => {
console.info(message);
consoleDefaultLog.info = (...args: unknown[]): void => {
console.info(args);
};

consoleDefaultLog.verbose = (message: string): void => {
console.debug(message);
consoleDefaultLog.verbose = (...args: unknown[]): void => {
console.debug(args);
};

export const consoleLogger: Logger = consoleDefaultLog;

export const createAppInsightsLogger = (telemetryClient: TelemetryClient): Logger => {
const appInsightsLogger = (message: string): void => {
const appInsightsLogger = (...args: unknown[]): void => {
telemetryClient.trackTrace({
message,
message: stringify(args),
severity: SeverityLevel.Information,
});
};

appInsightsLogger.error = (message: string): void => {
appInsightsLogger.error = (...args: unknown[]): void => {
telemetryClient.trackTrace({
message,
message: stringify(args),
severity: SeverityLevel.Error,
});
};

appInsightsLogger.warn = (message: string): void => {
appInsightsLogger.warn = (...args: unknown[]): void => {
telemetryClient.trackTrace({
message,
message: stringify(args),
severity: SeverityLevel.Warning,
});
};

appInsightsLogger.info = (message: string): void => {
appInsightsLogger.info = (...args: unknown[]): void => {
telemetryClient.trackTrace({
message,
message: stringify(args),
severity: SeverityLevel.Information,
});
};

appInsightsLogger.verbose = (message: string): void => {
appInsightsLogger.verbose = (...args: unknown[]): void => {
telemetryClient.trackTrace({
message,
message: stringify(args),
severity: SeverityLevel.Verbose,
});
};
Expand Down
50 changes: 3 additions & 47 deletions src/error/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,9 @@
import { Context } from '@azure/functions';

import { Options } from '../middleware';
import { stringify } from '../util/stringify';
import { ApplicationError } from './ApplicationError';

type ErrorWithMessage = {
message: string;
stack?: string;
};

const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
};

const logErrorObject = (error: object | null, context: Context) => {
if (error === null) {
context.log.error('The provided error was eq to null - unable to log a specific error-message');
return;
}

if (isErrorWithMessage(error)) {
context.log.error({ message: error.message, stack: error.stack });
} else {
try {
const errorAsJson = JSON.stringify(error);
if (errorAsJson === '{}') {
context.log.error(error.toString());
} else {
context.log.error(errorAsJson); // Log the JSON string of the error object
}
} catch (_) {
//Fallback in case there's an error stringify
context.log.error(error.toString());
}
}
};

export const errorHandler = (
error: unknown,
context: Context,
Expand All @@ -55,16 +19,8 @@ export const errorHandler = (
};
}

switch (typeof error) {
case 'string':
context.log.error(error);
break;
case 'object':
logErrorObject(error, context);
break;
default:
context.log(`The error object has a type, that is not suitable for logging: ${typeof error}`);
}
const errorAsString = stringify(error);
context.log.error(errorAsString);

if (opts?.errorResponseHandler === undefined) {
return {
Expand Down
63 changes: 63 additions & 0 deletions src/util/stringify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ApplicationError } from '../error';
import { stringify } from './stringify';

describe('stringify', () => {
describe('when provided with a single argument', () => {
it('returns the input string as is', () => {
const input = 'test string';
expect(stringify(input)).toBe(input);
});

it('stringifies an Error object with message and stack', () => {
const error = new ApplicationError('conflict', 409);
const result = stringify(error);
expect(result).toContain(error.message);
});

it('stringifies a plain object correctly', () => {
const input = { key: 'value' };
expect(stringify(input)).toBe(JSON.stringify(input));
});

it('returns object.toString() for an empty object', () => {
const input = {};
expect(stringify(input)).toBe('[object Object]');
});

it('returns error message for null', () => {
expect(stringify(null)).toBe('The provided error was eq to null - unable to log a specific error-message');
});

it('returns the number as a string for numbers', () => {
expect(stringify(123)).toBe('123');
});

it('returns the boolean value as a string for booleans', () => {
expect(stringify(true)).toBe('true');
expect(stringify(false)).toBe('false');
});

it('handles symbols correctly', () => {
expect(stringify(Symbol())).toContain('not suitable for logging: symbol');
});

it('handles functions correctly', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
expect(stringify(function () {})).toContain('not suitable for logging: function');
});

it('handles cyclic objects by using toString', () => {
const obj: any = {};
obj.cycle = obj;
expect(stringify(obj)).toBe(obj.toString());
});
});

describe('when provided with multiple arguments', () => {
it('joins stringified arguments with spaces', () => {
const arg1 = 'Hello';
const arg2 = { message: 'World' };
expect(stringify(arg1, arg2)).toBe('Hello {"message":"World"}');
});
});
});
76 changes: 76 additions & 0 deletions src/util/stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
type ErrorWithMessage = {
message: string;
stack?: string;
};

const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
};

const stringifyObject = (error: object | null): string => {
if (error === null) {
return 'The provided error was eq to null - unable to log a specific error-message';
}

if (error === undefined) {
return 'The provided error was eq to undefined - unable to log a specific error-message';
}

if (isErrorWithMessage(error)) {
return JSON.stringify({ message: error.message, stack: error.stack });
} else {
try {
const errorAsJson = JSON.stringify(error);
if (errorAsJson === '{}') {
return error.toString();
} else {
return errorAsJson;
}
} catch (_) {
//Fallback in case there's an error stringify
return error.toString();
}
}
};

const unknownToString = (input: unknown) => {
if (typeof input === 'object' && input !== null && 'toString' in input) return input.toString();
return String(input);
};

const stringifySingleArgument = (arg: unknown): string => {
switch (typeof arg) {
case 'string':
return arg;
case 'object':
return stringifyObject(arg);
case 'number':
return arg.toString();
case 'boolean':
return String(arg);
default:
return `The error object with value ${unknownToString(
arg,
)} has a type, that is not suitable for logging: ${typeof arg}`;
}
};

export const stringify = (...args: unknown[]): string => {
switch (typeof args) {
case 'string':
return args;
case 'object':
if (Array.isArray(args)) {
return args.map((element) => stringifySingleArgument(element)).join(' ');
}

return stringifyObject(args[0]);
default:
return `The error object has a type, that is not suitable for logging: ${typeof args}`;
}
};

0 comments on commit 54c991f

Please sign in to comment.