Skip to content

Commit

Permalink
[AXON-41] Implement AtlascodeErrorBoundary
Browse files Browse the repository at this point in the history
* Add support to receive rendering error events from UI
* Add AtlascodeErrorBoundary component
* Put the new boundary to Settings page
  • Loading branch information
sdzh-atlassian committed Dec 24, 2024
1 parent 1a9d4eb commit 3a11fea
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 146 deletions.
8 changes: 8 additions & 0 deletions src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ScreenEvent, TrackEvent, UIEvent } from './analytics-node-client/src/ty
import { DetailedSiteInfo, isEmptySiteInfo, Product, ProductJira, SiteInfo } from './atlclients/authInfo';
import { BitbucketIssuesTreeViewId, PullRequestTreeViewId } from './constants';
import { Container } from './container';
import { UIErrorInfo } from './analyticsTypes';

// IMPORTANT
// Make sure there is a corresponding event with the correct attributes in the Data Portal for any event created here.
Expand Down Expand Up @@ -255,6 +256,13 @@ export async function viewScreenEvent(

// UI Events

export async function uiErrorEvent(errorInfo: UIErrorInfo): Promise<TrackEvent> {
const e = trackEvent('failedTest', 'ui', {
attributes: { ...errorInfo },
});
return e;
}

export async function bbIssuesPaginationEvent(): Promise<UIEvent> {
const e = {
tenantIdType: null,
Expand Down
20 changes: 20 additions & 0 deletions src/analyticsTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// TODO: move this with other analytics stuff into a separate folder
// not doing it now to prevent too many import changes

/**
* Names of the channels used for routing analytics events in UI
*/
export enum AnalyticsChannels {
AtlascodeUiErrors = 'atlascode.ui.errors',
}

export type UIAnalyticsContext = {
view: string;
};

export type UIErrorInfo = UIAnalyticsContext & {
stack: string;
errorName: string;
errorMessage: string;
errorCause: string;
};
2 changes: 2 additions & 0 deletions src/lib/analyticsApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UIErrorInfo } from '../analyticsTypes';
import { DetailedSiteInfo, Product, SiteInfo } from '../atlclients/authInfo';

export interface AnalyticsApi {
Expand Down Expand Up @@ -48,4 +49,5 @@ export interface AnalyticsApi {
fireOpenSettingsButtonEvent(source: string): Promise<void>;
fireExploreFeaturesButtonEvent(source: string): Promise<void>;
firePipelineRerunEvent(site: DetailedSiteInfo, source: string): Promise<void>;
fireUIErrorEvent(errorInfo: UIErrorInfo): Promise<void>;
}
6 changes: 6 additions & 0 deletions src/lib/ipc/fromUI/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReducerAction } from '@atlassianlabs/guipi-core-controller';
import { MinimalIssueOrKeyAndSite } from '@atlassianlabs/jira-pi-common-models';
import { DetailedSiteInfo } from '../../../atlclients/authInfo';
import { FeedbackData, PMFData } from '../models/common';
import { UIErrorInfo } from '../../../analyticsTypes';

export enum CommonActionType {
SubmitPMF = 'pmfSubmit',
Expand All @@ -14,9 +15,11 @@ export enum CommonActionType {
CopyLink = 'copyLink',
OpenJiraIssue = 'openJiraIssue',
Cancel = 'cancelInFlight',
SendAnalytics = 'sendAnalytics',
}

export type CommonAction =
| ReducerAction<CommonActionType.SendAnalytics, SendAnalyticsAction>
| ReducerAction<CommonActionType.SubmitPMF, PMFSubmitAction>
| ReducerAction<CommonActionType.OpenPMFSurvey>
| ReducerAction<CommonActionType.DismissPMFLater>
Expand All @@ -28,6 +31,9 @@ export type CommonAction =
| ReducerAction<CommonActionType.OpenJiraIssue, OpenJiraIssueAction>
| ReducerAction<CommonActionType.Cancel, CancelAction>;

export interface SendAnalyticsAction {
errorInfo: UIErrorInfo;
}
export interface PMFSubmitAction {
pmfData: PMFData;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class BitbucketIssueWebviewController implements WebviewController<Bitbuc
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.Cancel:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export class CreateBitbucketIssueWebviewController implements WebviewController<
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.Cancel:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ export class ConfigWebviewController implements WebviewController<SectionChangeM
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.ExternalLink:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export class OnboardingWebviewController implements WebviewController<SectionCha
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.SubmitFeedback:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export class CreatePullRequestWebviewController implements WebviewController<Wor
}
break;

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.ExternalLink:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ export class PullRequestDetailsWebviewController implements WebviewController<Pu
}
break;

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.SubmitFeedback:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export class StartWorkWebviewController implements WebviewController<StartWorkIs
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.ExternalLink:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class WelcomeWebviewController implements WebviewController<WelcomeInitMe
break;
}

case CommonActionType.SendAnalytics:
case CommonActionType.CopyLink:
case CommonActionType.OpenJiraIssue:
case CommonActionType.ExternalLink:
Expand Down
70 changes: 70 additions & 0 deletions src/react/atlascode/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useCallback } from 'react';
// eslint-disable-next-line no-restricted-imports
import { AnalyticsErrorBoundary, AnalyticsListener, UIAnalyticsEvent } from '@atlaskit/analytics-next';
import { CommonActionType } from 'src/lib/ipc/fromUI/common';
import { AnalyticsChannels, UIAnalyticsContext } from 'src/analyticsTypes';

const STACK_LIMIT = 150;
const COMPONENT_GLOBAL = 'global';

export type AtlascodeErrorBoundaryProps = {
postMessageFunc: (action: any) => void;
context: UIAnalyticsContext;
children?: React.ReactNode;
};

/**
* Global error boundary for Atlascode UI components
*
* Put this somewhere close to the root of your component tree.
* It will catch all rendering errors in the subtree and send them to the main process.
*
* Add `AnalyticsErrorBoundary` around the specific components you'd like to handle and track separately.
*
* @param {AtlascodeErrorBoundaryProps} props
* @returns
*/
export const AtlascodeErrorBoundary: React.FC<AtlascodeErrorBoundaryProps> = ({
children,
postMessageFunc,
context,
}: AtlascodeErrorBoundaryProps) => {
const onRenderError = useCallback(
(event: UIAnalyticsEvent) => {
const { error, info, ...rest } = event.payload.attributes;
postMessageFunc({
type: CommonActionType.SendAnalytics,
errorInfo: {
...context,
errorType: error.name,
errorMessage: error.message,
errorCause: error.cause,
componentStack: simplifyStack(
info.componentStack,
(line) => line.match(/in (.*) \(created by (.*)\)/)?.[1],
),
stack: simplifyStack(error.stack, (line) => line.match(/at (.*) \((.*)\)/)?.[1]),
...rest,
},
});
},
[postMessageFunc, context],
);

return (
<AnalyticsListener channel={AnalyticsChannels.AtlascodeUiErrors} onEvent={onRenderError}>
<AnalyticsErrorBoundary
channel={AnalyticsChannels.AtlascodeUiErrors}
data={{ component: COMPONENT_GLOBAL }}
>
{children}
</AnalyticsErrorBoundary>
</AnalyticsListener>
);
};

const simplifyStack = (stack: string, extractFunc: (line: string) => string | undefined) => {
const line = stack.split('\n').map(extractFunc).filter(Boolean).join(' < ');

return line.length > STACK_LIMIT ? line.substring(0, STACK_LIMIT) + '...' : line;
};
Loading

0 comments on commit 3a11fea

Please sign in to comment.