mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
FSL-1.0-Apache-2.0 grants us the code, not the name. "Sentry" is Functional Software's trademark, so every place the running app says it to a user -- page titles, onboarding, empty states, toasts, settings copy, the self-hosted footer -- now says Sentinel. Attribution is the other half of the deal and stays untouched: upstream copyright headers, LICENSE, NOTICE. So does anything where the mark is a fact rather than a brand -- @sentry/* packages, sentry-cli, the wizard and bundler plugins, docs.sentry.io, the whole gettingStartedDocs tree. Renaming those would leave instructions that no longer work. Nothing structural moved. No import, identifier, route path, feature flag, API field or test id changed; the diff is 312 lines in and 312 out. Colocated specs were updated in lockstep with the copy they assert. Marketing and community links point at hanzo.ai, only where a real page exists to point at. Co-authored-by: Hanzo Dev <[email protected]>
253 lines
6.9 KiB
TypeScript
253 lines
6.9 KiB
TypeScript
import {Component, Fragment} from 'react';
|
|
import styled from '@emotion/styled';
|
|
import * as qs from 'query-string';
|
|
|
|
import bgPattern from 'sentry-images/spot/mobile-hero.jpg';
|
|
|
|
import {
|
|
addErrorMessage,
|
|
addLoadingMessage,
|
|
addSuccessMessage,
|
|
} from 'sentry/actionCreators/indicator';
|
|
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
|
|
import type {Client} from 'sentry/api';
|
|
import Access from 'sentry/components/acl/access';
|
|
import {Button} from 'sentry/components/button';
|
|
import ButtonBar from 'sentry/components/buttonBar';
|
|
import EmailField from 'sentry/components/forms/fields/emailField';
|
|
import Form from 'sentry/components/forms/form';
|
|
import List from 'sentry/components/list';
|
|
import ListItem from 'sentry/components/list/listItem';
|
|
import {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization} from 'sentry/types';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import withApi from 'sentry/utils/withApi';
|
|
|
|
type Props = ModalRenderProps & {
|
|
api: Client;
|
|
matchedUserAgentString: string;
|
|
organization: Organization;
|
|
};
|
|
|
|
type State = {
|
|
askTeammate: boolean;
|
|
};
|
|
|
|
class SuggestProjectModal extends Component<Props, State> {
|
|
state: State = {
|
|
askTeammate: false,
|
|
};
|
|
|
|
handleGetStartedClick = () => {
|
|
const {matchedUserAgentString, organization} = this.props;
|
|
trackAnalytics('growth.clicked_mobile_prompt_setup_project', {
|
|
matchedUserAgentString,
|
|
organization,
|
|
});
|
|
};
|
|
|
|
handleAskTeammate = () => {
|
|
const {matchedUserAgentString, organization} = this.props;
|
|
this.setState({askTeammate: true});
|
|
trackAnalytics('growth.clicked_mobile_prompt_ask_teammate', {
|
|
matchedUserAgentString,
|
|
organization,
|
|
});
|
|
};
|
|
|
|
goBack = () => {
|
|
this.setState({askTeammate: false});
|
|
};
|
|
|
|
handleSubmitSuccess = () => {
|
|
const {matchedUserAgentString, organization, closeModal} = this.props;
|
|
addSuccessMessage('Notified teammate successfully');
|
|
trackAnalytics('growth.submitted_mobile_prompt_ask_teammate', {
|
|
matchedUserAgentString,
|
|
organization,
|
|
});
|
|
closeModal();
|
|
};
|
|
|
|
handlePreSubmit = () => {
|
|
addLoadingMessage(t('Submitting\u2026'));
|
|
};
|
|
|
|
handleSubmitError = () => {
|
|
addErrorMessage(t('Error notifying teammate'));
|
|
};
|
|
|
|
renderAskTeammate() {
|
|
const {Body, organization} = this.props;
|
|
return (
|
|
<Body>
|
|
<Form
|
|
apiEndpoint={`/organizations/${organization.slug}/request-project-creation/`}
|
|
apiMethod="POST"
|
|
submitLabel={t('Send')}
|
|
onSubmitSuccess={this.handleSubmitSuccess}
|
|
onSubmitError={this.handleSubmitError}
|
|
onPreSubmit={this.handlePreSubmit}
|
|
extraButton={
|
|
<BackWrapper>
|
|
<StyledBackButton onClick={this.goBack}>{t('Back')}</StyledBackButton>
|
|
</BackWrapper>
|
|
}
|
|
>
|
|
<p>
|
|
{t('Let the right folks know about Sentinel Mobile Application Monitoring.')}
|
|
</p>
|
|
<EmailField
|
|
required
|
|
name="targetUserEmail"
|
|
inline={false}
|
|
label={t('Email Address')}
|
|
placeholder="[email protected]"
|
|
stacked
|
|
/>
|
|
</Form>
|
|
</Body>
|
|
);
|
|
}
|
|
|
|
renderMain() {
|
|
const {Body, Footer, organization} = this.props;
|
|
|
|
const paramString = qs.stringify({
|
|
referrer: 'suggest_project',
|
|
category: 'mobile',
|
|
});
|
|
|
|
const newProjectLink = `/organizations/${organization.slug}/projects/new/?${paramString}`;
|
|
|
|
return (
|
|
<Fragment>
|
|
<Body>
|
|
<ModalContainer>
|
|
<SmallP>
|
|
{t(
|
|
"Sentinel for Mobile shows a holistic overview of your application's health in real time. So you can correlate errors with releases, tags, and devices to solve problems quickly, decrease churn, and improve user retention."
|
|
)}
|
|
</SmallP>
|
|
|
|
<StyledList symbol="bullet">
|
|
<ListItem>
|
|
{tct(
|
|
'[see:See] session data, version adoption, and user impact by every release.',
|
|
{
|
|
see: <strong />,
|
|
}
|
|
)}
|
|
</ListItem>
|
|
<ListItem>
|
|
{tct(
|
|
'[solve:Solve] issues quickly with full context: contextualized stack traces, events that lead to the error, client, hardware information, and the very commit that introduced the error.',
|
|
{
|
|
solve: <strong />,
|
|
}
|
|
)}
|
|
</ListItem>
|
|
<ListItem>
|
|
{tct(
|
|
'[learn:Learn] and analyze event data to reduce regressions and ultimately improve user adoption and engagement.',
|
|
{
|
|
learn: <strong />,
|
|
}
|
|
)}
|
|
</ListItem>
|
|
</StyledList>
|
|
|
|
<SmallP>{t('And guess what? Setup takes less than five minutes.')}</SmallP>
|
|
</ModalContainer>
|
|
</Body>
|
|
<Footer>
|
|
<Access access={['project:write']}>
|
|
{({hasAccess}) => (
|
|
<ButtonBar gap={1}>
|
|
<Button
|
|
priority={hasAccess ? 'default' : 'primary'}
|
|
onClick={this.handleAskTeammate}
|
|
>
|
|
{t('Tell a Teammate')}
|
|
</Button>
|
|
{hasAccess && (
|
|
<Button
|
|
href={newProjectLink}
|
|
onClick={this.handleGetStartedClick}
|
|
priority="primary"
|
|
>
|
|
{t('Get Started')}
|
|
</Button>
|
|
)}
|
|
</ButtonBar>
|
|
)}
|
|
</Access>
|
|
</Footer>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {Header} = this.props;
|
|
const {askTeammate} = this.state;
|
|
const header = askTeammate ? t('Tell a Teammate') : t('Try Sentinel for Mobile');
|
|
return (
|
|
<Fragment>
|
|
<Header>
|
|
<PatternHeader />
|
|
<Title>{header}</Title>
|
|
</Header>
|
|
{this.state.askTeammate ? this.renderAskTeammate() : this.renderMain()}
|
|
</Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
const ModalContainer = styled('div')`
|
|
display: grid;
|
|
gap: ${space(3)};
|
|
|
|
code {
|
|
word-break: break-word;
|
|
}
|
|
`;
|
|
|
|
const Title = styled('h3')`
|
|
margin-top: ${space(2)};
|
|
margin-bottom: ${space(3)};
|
|
`;
|
|
|
|
const SmallP = styled('p')`
|
|
margin: 0;
|
|
`;
|
|
|
|
const PatternHeader = styled('div')`
|
|
margin: -${space(4)} -${space(4)} 0 -${space(4)};
|
|
border-radius: 7px 7px 0 0;
|
|
background-image: url(${bgPattern});
|
|
background-size: 475px;
|
|
background-color: black;
|
|
background-repeat: no-repeat;
|
|
overflow: hidden;
|
|
background-position: center bottom;
|
|
height: 156px;
|
|
`;
|
|
|
|
const StyledList = styled(List)`
|
|
li {
|
|
padding-left: ${space(3)};
|
|
}
|
|
`;
|
|
|
|
const BackWrapper = styled('div')`
|
|
width: 100%;
|
|
margin-right: ${space(1)};
|
|
`;
|
|
|
|
const StyledBackButton = styled(Button)`
|
|
float: right;
|
|
`;
|
|
|
|
export default withApi(SuggestProjectModal);
|