Skip to content

Commit 028e531

Browse files
authored
feat(ourlogs): Empty state for logs (#97455)
### Summary Adds onboarding / empty state for logs so it's easier for users to get setup.
1 parent 4747b84 commit 028e531

File tree

12 files changed

+872
-70
lines changed

12 files changed

+872
-70
lines changed

static/app/components/onboarding/gettingStartedDoc/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export interface Docs<PlatformOptions extends BasePlatformOptions = BasePlatform
223223
feedbackOnboardingCrashApi?: OnboardingConfig<PlatformOptions>;
224224
feedbackOnboardingJsLoader?: OnboardingConfig<PlatformOptions>;
225225
feedbackOnboardingNpm?: OnboardingConfig<PlatformOptions>;
226+
logsOnboarding?: OnboardingConfig<PlatformOptions>;
226227
mcpOnboarding?: OnboardingConfig<PlatformOptions>;
227228
performanceOnboarding?: OnboardingConfig<PlatformOptions>;
228229
platformOptions?: PlatformOptions;

static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
featureFlagOnboardingPlatforms,
77
feedbackOnboardingPlatforms,
88
replayPlatforms,
9+
withLoggingOnboarding,
910
withPerformanceOnboarding,
1011
} from 'sentry/data/platformCategories';
1112
import type {Organization} from 'sentry/types/organization';
@@ -16,7 +17,7 @@ import {useProjectKeys} from 'sentry/utils/useProjectKeys';
1617
type Props = {
1718
orgSlug: Organization['slug'];
1819
platform: PlatformIntegration;
19-
productType?: 'feedback' | 'replay' | 'performance' | 'featureFlags';
20+
productType?: 'feedback' | 'replay' | 'performance' | 'featureFlags' | 'logs';
2021
projSlug?: Project['slug'];
2122
};
2223

@@ -38,6 +39,7 @@ export function useLoadGettingStarted({
3839
);
3940

4041
const projectKeys = useProjectKeys({orgSlug, projSlug});
42+
4143
const platformPath = getPlatformPath(platform);
4244

4345
useEffect(() => {
@@ -47,6 +49,7 @@ export function useLoadGettingStarted({
4749
!platformPath ||
4850
(productType === 'replay' && !replayPlatforms.includes(platform.id)) ||
4951
(productType === 'performance' && !withPerformanceOnboarding.has(platform.id)) ||
52+
(productType === 'logs' && !withLoggingOnboarding.has(platform.id)) ||
5053
(productType === 'feedback' &&
5154
!feedbackOnboardingPlatforms.includes(platform.id)) ||
5255
(productType === 'featureFlags' &&

static/app/data/platformCategories.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,21 @@ export const withoutPerformanceSupport: Set<PlatformKey> = new Set([
304304
'xbox',
305305
]);
306306

307+
// List of platforms that have logging onboarding checklist content
308+
export const withLoggingOnboarding: Set<PlatformKey> = new Set([]);
309+
310+
// List of platforms that do not have logging support. We make use of this list in the product to not provide any Logging
311+
export const withoutLoggingSupport: Set<PlatformKey> = new Set([
312+
'cocoa-objc',
313+
'cocoa-swift',
314+
'elixir',
315+
'dotnet',
316+
'php-symfony',
317+
'unity',
318+
'unreal',
319+
'native',
320+
]);
321+
307322
export const profiling: PlatformKey[] = [
308323
'android',
309324
'apple',

static/app/utils/analytics/logsAnalyticsEvent.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {Organization} from 'sentry/types/organization';
2+
import type {PlatformKey} from 'sentry/types/project';
23

34
export enum LogsAnalyticsPageSource {
45
EXPLORE_LOGS = 'explore',
@@ -48,6 +49,22 @@ export type LogsAnalyticsEventParameters = {
4849
log_id: string;
4950
page_source: LogsAnalyticsPageSource;
5051
};
52+
53+
'logs.tracing_onboarding': {
54+
organization: Organization;
55+
platform: PlatformKey | 'unknown';
56+
supports_onboarding_checklist: boolean;
57+
};
58+
59+
'logs.tracing_onboarding_performance_docs_viewed': {
60+
organization: Organization;
61+
platform: PlatformKey | 'unknown';
62+
};
63+
64+
'logs.tracing_onboarding_platform_docs_viewed': {
65+
organization: Organization;
66+
platform: PlatformKey | 'unknown';
67+
};
5168
};
5269

5370
type LogsAnalyticsEventKey = keyof LogsAnalyticsEventParameters;
@@ -59,6 +76,11 @@ export const logsAnalyticsEventMap: Record<LogsAnalyticsEventKey, string | null>
5976
'logs.explorer.metadata': 'Log Explorer Pageload Metadata',
6077
'logs.issue_details.drawer_opened': 'Issues Page Logs Drawer Opened',
6178
'logs.table.row_expanded': 'Expanded Log Row Details',
79+
'logs.tracing_onboarding': 'Logs Tracing Onboarding',
80+
'logs.tracing_onboarding_performance_docs_viewed':
81+
'Logs Tracing Onboarding Performance Docs Viewed',
82+
'logs.tracing_onboarding_platform_docs_viewed':
83+
'Logs Tracing Onboarding Platform Docs Viewed',
6284
'logs.save_as': 'Logs Save As',
6385
'logs.save_query_modal': 'Logs Save Query Modal',
6486
};

static/app/utils/eventWaiter.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type FirstIssue = null | boolean | Group;
2020
export interface EventWaiterProps {
2121
api: Client;
2222
children: (props: {firstIssue: FirstIssue}) => React.ReactNode;
23-
eventType: 'error' | 'transaction' | 'replay' | 'profile';
23+
eventType: 'error' | 'transaction' | 'replay' | 'profile' | 'log';
2424
organization: Organization;
2525
project: Project;
2626
disabled?: boolean;
@@ -42,6 +42,8 @@ function getFirstEvent(eventType: EventWaiterProps['eventType'], resp: Project)
4242
return resp.hasReplays;
4343
case 'profile':
4444
return resp.hasProfiles;
45+
case 'log':
46+
return resp.hasLogs;
4547
default:
4648
return null;
4749
}
@@ -123,6 +125,8 @@ class EventWaiter extends Component<EventWaiterProps, EventWaiterState> {
123125
firstIssue = Boolean(firstEvent);
124126
} else if (eventType === 'profile') {
125127
firstIssue = Boolean(firstEvent);
128+
} else if (eventType === 'log') {
129+
firstIssue = Boolean(firstEvent);
126130
}
127131

128132
if (onIssueReceived) {

static/app/views/explore/logs/content.spec.tsx

Lines changed: 126 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
import {createLogFixtures, initializeLogsTest} from 'sentry-fixture/log';
2+
import {ProjectKeysFixture} from 'sentry-fixture/projectKeys';
3+
import {TeamFixture} from 'sentry-fixture/team';
24

35
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
46

7+
import * as useRecentCreatedProjectHook from 'sentry/components/onboarding/useRecentCreatedProject';
8+
import ProjectsStore from 'sentry/stores/projectsStore';
9+
import TeamStore from 'sentry/stores/teamStore';
10+
import type {Organization} from 'sentry/types/organization';
11+
import type {Project} from 'sentry/types/project';
512
import {LOGS_AUTO_REFRESH_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
13+
import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
614

715
import LogsPage from './content';
816

917
describe('LogsPage', function () {
10-
const {organization, project, setupPageFilters, setupEventsMock} = initializeLogsTest();
11-
12-
setupPageFilters();
18+
let organization: Organization;
19+
let project: Project;
20+
let testDate: Date;
1321

1422
let eventTableMock: jest.Mock;
1523
let eventStatsMock: jest.Mock;
1624

17-
// Standard log fixtures for consistent testing
18-
const testDate = new Date('2024-01-15T10:00:00.000Z');
19-
const {baseFixtures} = createLogFixtures(organization, project, testDate);
20-
2125
beforeEach(function () {
2226
MockApiClient.clearMockResponses();
27+
const {
28+
organization: _organization,
29+
project: _project,
30+
setupPageFilters,
31+
setupEventsMock,
32+
} = initializeLogsTest();
33+
34+
organization = _organization;
35+
project = _project;
36+
37+
setupPageFilters();
38+
39+
// Standard log fixtures for consistent testing
40+
testDate = new Date('2024-01-15T10:00:00.000Z');
41+
const {baseFixtures} = createLogFixtures(organization, project, testDate);
2342

2443
eventTableMock = setupEventsMock(baseFixtures.slice(0, 2));
2544

@@ -79,6 +98,56 @@ describe('LogsPage', function () {
7998
expect(table).toHaveTextContent(/User login successful/);
8099
});
81100

101+
it('should show onboarding when project is not onboarded', async function () {
102+
ProjectsStore.reset();
103+
const {
104+
organization: onboardingOrganization,
105+
project: onboardingProject,
106+
setupPageFilters: onboardingSetupPageFilters,
107+
} = initializeLogsTest({
108+
project: {
109+
hasLogs: false,
110+
platform: 'javascript-react',
111+
},
112+
});
113+
TeamStore.loadInitialData([TeamFixture()]);
114+
const projectSlugMock = MockApiClient.addMockResponse({
115+
url: `/projects/${onboardingOrganization.slug}/${onboardingProject.slug}/keys/`,
116+
method: 'GET',
117+
body: [ProjectKeysFixture()[0]],
118+
});
119+
const sdkMock = MockApiClient.addMockResponse({
120+
url: `/organizations/${onboardingOrganization.slug}/sdks/`,
121+
method: 'GET',
122+
body: [],
123+
});
124+
125+
jest
126+
.spyOn(useRecentCreatedProjectHook, 'useRecentCreatedProject')
127+
.mockImplementation(() => {
128+
return {
129+
project: onboardingProject,
130+
isProjectActive: true,
131+
};
132+
});
133+
134+
onboardingSetupPageFilters();
135+
136+
render(<LogsPage />, {
137+
organization,
138+
initialRouterConfig: {
139+
location: `/organizations/${organization.slug}/explore/logs/`,
140+
},
141+
});
142+
143+
expect(projectSlugMock).toHaveBeenCalled();
144+
expect(sdkMock).toHaveBeenCalled();
145+
146+
await waitFor(() => {
147+
expect(screen.getByText('Your Source for Log-ical Data')).toBeInTheDocument();
148+
});
149+
});
150+
82151
it('should call aggregates APIs as expected', async function () {
83152
render(<LogsPage />, {
84153
organization,
@@ -137,34 +206,39 @@ describe('LogsPage', function () {
137206
});
138207

139208
describe('autorefresh flag enabled', () => {
140-
const {
141-
organization: autorefreshOrganization,
142-
setupEventsMock: _setupEventsMock,
143-
setupTraceItemsMock: _setupTraceItemsMock,
144-
generateRouterConfig,
145-
routerConfig: initialRouterConfig,
146-
} = initializeLogsTest({
147-
refreshInterval: '10000', // 10 seconds, should not first events multiple times.
148-
liveRefresh: true,
149-
});
150-
151-
const {baseFixtures: autorefreshBaseFixtures} = createLogFixtures(
152-
autorefreshOrganization,
153-
project,
154-
testDate,
155-
{
156-
intervalMs: 24 * 60 * 60 * 1000, // 24 hours
157-
}
158-
);
209+
let initialRouterConfig: ReturnType<typeof initializeLogsTest>['routerConfig'];
210+
let autorefreshBaseFixtures: OurLogsResponseItem[];
211+
let setupTraceItemsMock: ReturnType<typeof initializeLogsTest>['setupTraceItemsMock'];
212+
let setupEventsMock: ReturnType<typeof initializeLogsTest>['setupEventsMock'];
159213

160214
beforeEach(() => {
161215
eventTableMock.mockClear();
162-
eventTableMock = _setupEventsMock(autorefreshBaseFixtures.slice(0, 5));
216+
217+
const {
218+
organization: autorefreshOrganization,
219+
setupEventsMock: _setupEventsMock,
220+
project: _project,
221+
setupTraceItemsMock: _setupTraceItemsMock,
222+
routerConfig,
223+
} = initializeLogsTest({
224+
refreshInterval: '10000', // 10 seconds, should not first events multiple times.
225+
liveRefresh: true,
226+
});
227+
organization = autorefreshOrganization;
228+
project = _project;
229+
initialRouterConfig = routerConfig;
230+
setupTraceItemsMock = _setupTraceItemsMock;
231+
setupEventsMock = _setupEventsMock;
232+
const {baseFixtures} = createLogFixtures(organization, project, testDate, {
233+
intervalMs: 24 * 60 * 60 * 1000, // 24 hours
234+
});
235+
eventTableMock = _setupEventsMock(baseFixtures.slice(0, 5));
236+
autorefreshBaseFixtures = baseFixtures;
163237
});
164238

165239
it('enables autorefresh when Switch is clicked', async function () {
166240
render(<LogsPage />, {
167-
organization: autorefreshOrganization,
241+
organization,
168242
initialRouterConfig,
169243
});
170244

@@ -185,10 +259,17 @@ describe('LogsPage', function () {
185259

186260
it('pauses auto-refresh when enabled switch is clicked', async function () {
187261
const {router} = render(<LogsPage />, {
188-
organization: autorefreshOrganization,
189-
initialRouterConfig: generateRouterConfig({
190-
[LOGS_AUTO_REFRESH_KEY]: 'enabled',
191-
}),
262+
organization,
263+
initialRouterConfig: {
264+
...initialRouterConfig,
265+
location: {
266+
...initialRouterConfig.location,
267+
query: {
268+
...initialRouterConfig.location.query,
269+
[LOGS_AUTO_REFRESH_KEY]: 'enabled',
270+
},
271+
},
272+
},
192273
});
193274
expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
194275

@@ -208,12 +289,19 @@ describe('LogsPage', function () {
208289
});
209290

210291
it('pauses auto-refresh when row is clicked', async function () {
211-
const rowDetailsMock = _setupTraceItemsMock(autorefreshBaseFixtures.slice(0, 1))[0];
292+
const rowDetailsMock = setupTraceItemsMock(autorefreshBaseFixtures.slice(0, 1))[0];
212293
const {router} = render(<LogsPage />, {
213-
organization: autorefreshOrganization,
214-
initialRouterConfig: generateRouterConfig({
215-
[LOGS_AUTO_REFRESH_KEY]: 'enabled',
216-
}),
294+
organization,
295+
initialRouterConfig: {
296+
...initialRouterConfig,
297+
location: {
298+
...initialRouterConfig.location,
299+
query: {
300+
...initialRouterConfig.location.query,
301+
[LOGS_AUTO_REFRESH_KEY]: 'enabled',
302+
},
303+
},
304+
},
217305
});
218306

219307
expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
@@ -243,7 +331,7 @@ describe('LogsPage', function () {
243331

244332
expect(eventTableMock).toHaveBeenCalledTimes(1);
245333
eventTableMock.mockClear();
246-
eventTableMock = _setupEventsMock(autorefreshBaseFixtures.slice(0, 5));
334+
eventTableMock = setupEventsMock(autorefreshBaseFixtures.slice(0, 5));
247335

248336
await userEvent.click(switchInput);
249337

0 commit comments

Comments
 (0)