Skip to content

test(feedback): Write some test for feedback utils and error returns #12519

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
42 changes: 42 additions & 0 deletions packages/core/src/feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
For reference, the fully built event looks something like this:

```json
{
"type": "feedback",
"event_id": "d2132d31b39445f1938d7e21b6bf0ec4",
"timestamp": 1597977777.6189718,
"dist": "1.12",
"platform": "javascript",
"environment": "production",
"release": 42,
"tags": { "transaction": "/organizations/:orgId/performance/:eventSlug/" },
"sdk": { "name": "name", "version": "version" },
"user": {
"id": "123",
"username": "user",
"email": "user@site.com",
"ip_address": "192.168.11.12"
},
"request": {
"url": null,
"headers": {
"user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15"
}
},
"contexts": {
"feedback": {
"message": "test message",
"contact_email": "test@example.com",
"type": "feedback"
},
"trace": {
"trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
"span_id": "FA90FDEAD5F74052",
"type": "trace"
},
"replay": {
"replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d"
}
}
}
```
1 change: 1 addition & 0 deletions packages/core/src/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function captureFeedback(
): string {
const { message, name, email, url, source, associatedEventId, tags } = params;

// See https://github.com/getsentry/sentry-javascript/blob/main/packages/core/src/feedback.md for an example feedback object
const feedbackEvent: FeedbackEvent = {
contexts: {
feedback: dropUndefinedKeys({
Expand Down
21 changes: 21 additions & 0 deletions packages/feedback/src/core/sendFeedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ describe('sendFeedback', () => {
patchedDecoder && delete global.window.TextDecoder;
});

it('fails when the message is falsey', async () => {
mockSdk();

expect(() =>
sendFeedback({
message: '',
}),
).toThrowError(new Error('Unable to submit feedback with empty message.'));
});

it('fails when the client is missing', async () => {
mockSdk();
getCurrentScope().setClient(undefined);

expect(() =>
sendFeedback({
message: 'mi',
}),
).toThrowError(new Error('No client setup, cannot send feedback.'));
});

it('sends feedback with minimal options', async () => {
mockSdk();
const mockTransport = jest.spyOn(getClient()!.getTransport()!, 'send');
Expand Down
45 changes: 2 additions & 43 deletions packages/feedback/src/core/sendFeedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const sendFeedback: SendFeedback = (
hint: EventHint & { includeReplay?: boolean } = { includeReplay: true },
): Promise<string> => {
if (!params.message) {
throw new Error('Unable to submit feedback with empty message');
throw new Error('Unable to submit feedback with empty message.');
}

// We want to wait for the feedback to be sent (or not)
Expand All @@ -26,6 +26,7 @@ export const sendFeedback: SendFeedback = (
if (params.tags && Object.keys(params.tags).length) {
getCurrentScope().setTags(params.tags);
}
// See https://github.com/getsentry/sentry-javascript/blob/main/packages/core/src/feedback.md for an example feedback object
const eventId = captureFeedback(
{
source: FEEDBACK_API_SOURCE,
Expand Down Expand Up @@ -67,45 +68,3 @@ export const sendFeedback: SendFeedback = (
});
});
};

/*
* For reference, the fully built event looks something like this:
* {
* "type": "feedback",
* "event_id": "d2132d31b39445f1938d7e21b6bf0ec4",
* "timestamp": 1597977777.6189718,
* "dist": "1.12",
* "platform": "javascript",
* "environment": "production",
* "release": 42,
* "tags": {"transaction": "/organizations/:orgId/performance/:eventSlug/"},
* "sdk": {"name": "name", "version": "version"},
* "user": {
* "id": "123",
* "username": "user",
* "email": "user@site.com",
* "ip_address": "192.168.11.12",
* },
* "request": {
* "url": None,
* "headers": {
* "user-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15"
* },
* },
* "contexts": {
* "feedback": {
* "message": "test message",
* "contact_email": "test@example.com",
* "type": "feedback",
* },
* "trace": {
* "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
* "span_id": "FA90FDEAD5F74052",
* "type": "trace",
* },
* "replay": {
* "replay_id": "e2d42047b1c5431c8cba85ee2a8ab25d",
* },
* },
* }
*/
25 changes: 25 additions & 0 deletions packages/feedback/src/modal/getUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
import type { User } from '@sentry/types';
import { getUser } from './getUser';

const currentUser: User = { username: 'current' };

describe('getUser', () => {
beforeEach(() => {
getCurrentScope().clear();
getIsolationScope().clear();
getGlobalScope().clear();
});

it('should prefer the user from the current scope', () => {
getCurrentScope().setUser(currentUser);

expect(getUser()).toEqual(expect.objectContaining(currentUser));
});

it('should return the empty user if no explicit user is set', () => {
getCurrentScope().setUser(null);

expect(getUser()).toEqual({ email: undefined, id: undefined, ip_address: undefined, username: undefined });
Copy link
Member Author

Choose a reason for hiding this comment

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

getUser() surprisingly has the return type User | undefined but when nothing is set we're getting a default empty user.

});
});
15 changes: 15 additions & 0 deletions packages/feedback/src/modal/getUser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
import type { User } from '@sentry/types';

export function getUser(): User | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

Wonder if we do anything similar in other integrations

const currentUser = getCurrentScope().getUser();
if (currentUser && Object.keys(currentUser).length) {
return currentUser;
}
const isolationUser = getIsolationScope().getUser();
if (isolationUser && Object.keys(isolationUser).length) {
return isolationUser;
}
const globalUser = getGlobalScope().getUser();
return globalUser;
Comment on lines +9 to +14
Copy link
Member Author

Choose a reason for hiding this comment

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

testing these two scopes was hard to do directly :(

}
18 changes: 3 additions & 15 deletions packages/feedback/src/modal/integration.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
import type { FeedbackFormData, FeedbackModalIntegration, IntegrationFn, User } from '@sentry/types';
import type { FeedbackFormData, FeedbackModalIntegration, IntegrationFn } from '@sentry/types';

import { h, render } from 'preact';
import * as hooks from 'preact/hooks';
import { DOCUMENT } from '../constants';
import { Dialog } from './components/Dialog';
import { createDialogStyles } from './components/Dialog.css';

function getUser(): User | undefined {
const currentUser = getCurrentScope().getUser();
const isolationUser = getIsolationScope().getUser();
const globalUser = getGlobalScope().getUser();
if (currentUser && Object.keys(currentUser).length) {
return currentUser;
}
if (isolationUser && Object.keys(isolationUser).length) {
return isolationUser;
}
return globalUser;
}
import { getUser } from './getUser';

export const feedbackModalIntegration = ((): FeedbackModalIntegration => {
return {
Expand Down
44 changes: 44 additions & 0 deletions packages/feedback/src/util/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getMissingFields } from './validate';

const BASE_FEEDBACK = {
message: '',
name: '',
email: '',
attachments: undefined,
};
const BASE_OPTIONS = {
emailLabel: 'Email',
isEmailRequired: false,
isNameRequired: false,
messageLabel: 'Message',
nameLabel: 'Name',
};

describe('validate', () => {
it('should be invalid when message is falsey', () => {
const result = getMissingFields(BASE_FEEDBACK, BASE_OPTIONS);

expect(result).toEqual(['Message']);
});

it('should be valid when message is filled out', () => {
const result = getMissingFields({ ...BASE_FEEDBACK, message: 'Hello world' }, BASE_OPTIONS);

expect(result).toEqual([]);
});

it('should be invalid when name & email are required', () => {
const result = getMissingFields(BASE_FEEDBACK, { ...BASE_OPTIONS, isNameRequired: true, isEmailRequired: true });

expect(result).toEqual(['Name', 'Email', 'Message']);
});

it('should be valid when name & email are required and not falsey', () => {
const result = getMissingFields(
{ message: 'Hello world', name: 'Alice', email: 'alice@example.com', attachments: undefined },
{ ...BASE_OPTIONS, isNameRequired: true, isEmailRequired: true },
);

expect(result).toEqual([]);
});
});
Loading