Skip to content

sdk-core, node: attribute validation #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ export class ApplicationInformationAttributeProvider implements BacktraceAttribu
this._applicationVersion = this._applicationVersion ?? (applicationData['version'] as string);
}

if (!this._application || !this._applicationVersion) {
throw new Error(
'Cannot find information about the package. Please define application and application.version attribute',
);
}

return {
package: applicationData,
[this.APPLICATION_ATTRIBUTE]: this._application,
Expand Down
49 changes: 49 additions & 0 deletions packages/sdk-core/src/BacktraceCoreClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ export abstract class BacktraceCoreClient<
}

public initialize() {
this.validateAttributes();

if (this.fileSystem && this.options.database?.createDatabaseDirectory) {
if (!this.options.database.path) {
throw new Error(
Expand Down Expand Up @@ -418,4 +420,51 @@ export abstract class BacktraceCoreClient<
private static destroy() {
this._instance = undefined;
}

private validateAttributes() {
function isNotEmptyString(v: unknown) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're introducing a nice way to validate attributes, but in our scenario, we don't need such complexity. What we can do is just simply test if those two attributes are defined. Based on what we want to verify and how easily we can achieve that, wouldn't it be better if we keep the smallest/simplest solution for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would you do this more "easily"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just simply

const attributes = this.attributeManager.ger()
const validAttributes = !!attributes["application"] && !!attributes["application.version"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified a bit

if (typeof v === 'string' && v) {
return;
}

return 'must be defined and not an empty string';
}

const validators = {
application: isNotEmptyString,
'application.version': isNotEmptyString,
} as const;

function validate(attributes: Record<string, unknown>) {
const errors: Record<string, string> = {};
for (const [attribute, validate] of Object.entries(validators)) {
const value = attributes[attribute];
const error = validate(value);
if (error) {
errors[attribute] = error;
}
}
return errors;
}

// Validate scoped attributes first. If they return no errors, there is no point
// in checking all attributes, which resolving of may be slower.
const scoped = this.attributeManager.get('scoped');
const scopedErrors = validate(scoped.attributes);
if (!Object.keys(scopedErrors).length) {
return;
}

const allAttributes = this.attributeManager.get();
const allErrors = validate(allAttributes.attributes);
if (!Object.keys(allErrors).length) {
return;
}

const attributeErrors = Object.entries(allErrors)
.map(([attribute, error]) => `${attribute}: ${error}`)
.join('\n');

throw new Error(`Following attributes are invalid:\n${attributeErrors}`);
}
}
100 changes: 100 additions & 0 deletions packages/sdk-core/tests/client/clientTests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { BacktraceReport, BacktraceStringAttachment } from '../../src/index.js';
import { AttributeManager } from '../../src/modules/attribute/AttributeManager.js';
import { BacktraceTestClient } from '../mocks/BacktraceTestClient.js';
import { testHttpClient } from '../mocks/testHttpClient.js';
describe('Client tests', () => {
afterEach(() => {
jest.restoreAllMocks();
});

describe('Send tests', () => {
const client = BacktraceTestClient.buildFakeClient();

Expand Down Expand Up @@ -128,4 +134,98 @@ describe('Client tests', () => {
);
});
});

describe('Validation tests', () => {
it('should throw on initialize when application and application.version attributes are missing', () => {
const instance = new BacktraceTestClient({}, testHttpClient);
expect(() => instance.initialize()).toThrow(/application: must be defined and not an empty string/);
expect(() => instance.initialize()).toThrow(/application.version: must be defined and not an empty string/);
});

it('should throw on initialize when application attribute is missing', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'scoped',
get: () => ({
'application.version': '1.2.3',
}),
},
]);
expect(() => instance.initialize()).toThrow(/application: must be defined and not an empty string/);
});

it('should throw on initialize when application.version attribute is missing', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'scoped',
get: () => ({
application: 'my-app',
}),
},
]);
expect(() => instance.initialize()).toThrow(/application.version: must be defined and not an empty string/);
});

it('should not throw on initialize when application and application.version attributes are defined as scoped', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'scoped',
get: () => ({
application: 'my-app',
'application.version': '1.2.3',
}),
},
]);
expect(() => instance.initialize()).not.toThrow();
});

it('should not throw on initialize when application and application.version attributes are defined as dynamic', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'dynamic',
get: () => ({
application: 'my-app',
'application.version': '1.2.3',
}),
},
]);
expect(() => instance.initialize()).not.toThrow();
});

it('should only test scoped attributes and not all when application and application.version attributes are defined as scoped', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'scoped',
get: () => ({
application: 'my-app',
'application.version': '1.2.3',
}),
},
]);

const getAttributesSpy = jest.spyOn(AttributeManager.prototype, 'get');
instance.initialize();

expect(getAttributesSpy).toHaveBeenCalledWith('scoped');
expect(getAttributesSpy).not.toHaveBeenCalledWith();
});

it('should test both scoped attributes and all when application and application.version attributes are defined as dynamic', () => {
const instance = new BacktraceTestClient({}, testHttpClient, [
{
type: 'dynamic',
get: () => ({
application: 'my-app',
'application.version': '1.2.3',
}),
},
]);

const getAttributesSpy = jest.spyOn(AttributeManager.prototype, 'get');
instance.initialize();

expect(getAttributesSpy).toHaveBeenCalledWith('scoped');
expect(getAttributesSpy).toHaveBeenCalledWith();
});
});
});
Loading