{"id":13631288,"url":"https://github.com/getsentry/frontend-handbook","last_synced_at":"2025-04-17T21:32:41.192Z","repository":{"id":66037031,"uuid":"180230180","full_name":"getsentry/frontend-handbook","owner":"getsentry","description":"Frontend at Sentry","archived":true,"fork":false,"pushed_at":"2020-07-02T14:45:49.000Z","size":73,"stargazers_count":10,"open_issues_count":1,"forks_count":2,"subscribers_count":54,"default_branch":"master","last_synced_at":"2025-04-08T10:08:23.400Z","etag":null,"topics":["tag-archived"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/getsentry.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"custom":["https://sentry.io/pricing/","https://sentry.io/"]}},"created_at":"2019-04-08T20:46:17.000Z","updated_at":"2023-08-30T00:06:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"4004b1bb-1aa7-4325-9b9e-96d440c38ff5","html_url":"https://github.com/getsentry/frontend-handbook","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getsentry%2Ffrontend-handbook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getsentry%2Ffrontend-handbook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getsentry%2Ffrontend-handbook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getsentry%2Ffrontend-handbook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/getsentry","download_url":"https://codeload.github.com/getsentry/frontend-handbook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249381006,"owners_count":21261227,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["tag-archived"],"created_at":"2024-08-01T22:02:19.103Z","updated_at":"2025-04-17T21:32:40.938Z","avatar_url":"https://github.com/getsentry.png","language":null,"funding_links":["https://sentry.io/pricing/","https://sentry.io/"],"categories":["Others"],"sub_categories":[],"readme":"# frontend-handbook\n\nFrontend at Sentry\n\n\u003e This guide covers how we write frontend code at Sentry, and is specifically focussed on the [Sentry](https://github.com/getsentry/sentry) and [Getsentry](https://github.com/getsentry/getsentry) codebases. It assumes you are using the eslint rules outlined by [eslint-config-sentry](https://github.com/getsentry/eslint-config-sentry); hence code style enforced by these linting rules will not be discussed here.\n\n## Quick links:\n\n- [Directory structure](#directory-structure)\n- [React](#react)\n- [CSS and Emotion](#css-and-emotion)\n- [State management](#state-management)\n- [Testing](#testing)\n- [Babel Syntax Plugins](#babel-syntax-plugins)\n- [New Syntax](#new-syntax)\n- [lodash](#lodash)\n- [Typescript](#typescript)\n- [Contributing](#contributing)\n\n## Directory structure\n\nThe frontend codebase is currently located under `src/sentry/static/sentry/app` in sentry and `static/getsentry` in getsentry. (We intend to align to `static/sentry` in future.)\n\n## React\n\n### Defining React components\n\nNew components use the class syntax, and the class field+arrow function method definition when they need to access this.\n\n```javascript\nclass Note extends React.Component {\n  static propTypes = {\n    author: PropTypes.object.isRequired,\n    onEdit: PropTypes.func.isRequired,\n  };\n\n  // Note that method is defined using an arrow function class field (to bind \"this\")\n  handleChange = value =\u003e {\n    let user = ConfigStore.get('user');\n\n    if (user.isSuperuser) {\n      this.props.onEdit(value);\n    }\n  };\n\n  render() {\n    let {content} = this.props; // use destructuring assignment for props\n\n    return \u003cdiv onChange={this.handleChange}\u003e{content}\u003c/div\u003e;\n  }\n}\n\nexport default Note;\n```\n\nSome older components use `createReactClass` and mixins, but this is deprecated.\n\n### Components vs views\n\nBoth the `app/components/` and `app/views` folders contain React components.\n\n- Use a view for UI that will typically not be reused in other parts of the codebase\n- Use a component for UI that is designed to be highly reusable.\n\nComponents should have an associated `.stories.js` file that documents how it should be used.\n\nRun Storybook locally with `yarn storybook` or view the hosted version at `https://storybook.getsentry.net/`\n\n### PropTypes\n\nUse them, be explicit, use the shared custom proptypes when possible.\n\nPrefer `Proptypes.arrayOf` to `PropTypes.array` and `PropTypes.shape` to `PropTypes.object`\n\nIf you’re passing Objects with an important, well defined set of keys (that your component relies on) then define them explicitly with `PropTypes.shape`:\n\n```javascript\nPropTypes.shape({\n  username: PropTypes.string.isRequired,\n  email: PropTypes.string\n})\n```\n\nIf you’re re-using a custom prop-type or passing around a common shared shape like an organization, project, or user, then be sure to import a proptype from our useful collection of custom ones! [https://github.com/getsentry/sentry/blob/master/src/sentry/static/sentry/app/sentryTypes.jsx](https://github.com/getsentry/sentry/blob/master/src/sentry/static/sentry/app/sentryTypes.jsx)\n\n### Event handlers\n\nWe use different prefixes to better distinguish event handlers from event callback props.\n\nUse the `handle` prefix for event handlers, e.g:\n\n```javascript\n\u003cButton onClick={this.handleDelete}/\u003e\n```\n\nFor event callback props passed to the component use the `on` prefix, e.g:\n\n```javascript\n\u003cButton onClick={this.props.onDelete}\u003e\n```\n\n## CSS and Emotion\n\n* Use Emotion, use the `theme` object.\n* The best styles are ones you don’t write - whenever possible use existing components.\n* New code should use the css-in-js library [e m o t i o n](https://emotion.sh/) - it lets you bind styles to elements without the indirection of global selectors. You don’t even need to open another file!\n* Take constants (z-indexes, paddings, colors) from [props.theme](https://github.com/getsentry/sentry/blob/master/src/sentry/static/sentry/app/utils/theme.jsx)\n\n```javascript\nimport styled from 'react-emotion';\n\nconst SomeComponent = styled('div')`\n  border-radius: 1.45em;\n  font-weight: bold;\n  z-index: ${p =\u003e p.theme.zIndex.modal};\n  padding: ${p =\u003e p.theme.grid}px ${p =\u003e p.theme.grid * 2}px;\n  border: 1px solid ${p =\u003e p.theme.borderLight};\n  color: ${p =\u003e p.theme.purple};\n  box-shadow: ${p =\u003e p.theme.dropShadowHeavy};\n`;\n\nexport default SomeComponent;\n```\n\n* Note that `reflexbox` (e.g. `Flex` and `Box`) is being deprecated, avoid using in new code.\n\n### `stylelint` Errors\n\n#### \"No duplicate selectors\"\nThis happens when you use a styled component as a selector, we need to tell stylelint that what we are interpolating is a selector by using comments to assist the linter. e.g.\n\n```javascript\n\nconst ButtonBar = styled(\"div\")`\n  ${/* sc-selector */Button) {\n     border-radius: 0;\n  }\n`;\n```\n\nSee https://styled-components.com/docs/tooling#interpolation-tagging for other tags and more information.\n\n## State management\n\nWe currently use [Reflux](https://github.com/reflux/refluxjs) for managing global state.\n\nReflux implements the unidirectional data flow pattern outlined by [Flux](https://facebook.github.io/flux/docs/overview.html). Stores are registered under `app/stores` and are used to store various pieces of data used by the application. Actions need to be registered under `app/actions`. We use action creator functions (under `app/actionCreators`) to dispatch actions. Reflux stores listen to actions and update themselves accordingly.\n\nWe are currently exploring alternatives to the `Reflux` library for future use.\n\n## Testing\n\nNote: Your filename needs to be .spec.jsx or jest won’t run it!\n\nWe have useful fixtures defined in [setup.js](https://github.com/getsentry/sentry/blob/master/tests/js/setup.js) Use these! If you are defining mock data in a repetitive way, it’s probably worth adding this this file. routerContext is a particularly useful one for providing the context object that most view are written to rely on.\n\nClient.addMockResponse is the best way to mock API requests. it’s [our code](https://github.com/getsentry/sentry/blob/master/src/sentry/static/sentry/app/__mocks__/api.jsx) so if it’s confusing you, just put `console.log()` statements into its logic!\n\nAn important gotcha in our testing environment is the way that enzyme modifies many aspects of the react lifecycle to evaluate synchronously (even when they’re usually async). This can lull you into a false sense of security when you trigger some logic and don’t find it reflected immediately afterwards in your assert logic.\n\nMarking your test method `async` and using the `await tick();` utility can let the event loop flush run events and fix this:\n\n```javascript\nwrapper.find('ExpandButton').simulate('click');\nawait tick();\nexpect(wrapper.find('CommitRow')).toHaveLength(2);\n```\n\n### Selectors\n\nIf you are writing jest tests, you can use a Component (and Styled Component) names as a selector. Additionally, if you need to use a DOM query selector, use `data-test-id` instead of a class name. We currently don’t, but it is something we can use babel to strip out during the build process.\n\n### Undefined `theme` properties in tests\n\nInstead of using `mount()` from `enzyme` ...use this: `import {mountWithTheme} from 'sentry-test/enzyme'` so that the component under test gets wrapped with a [`\u003cThemeProvider\u003e`](https://emotion.sh/docs/theming).\n\n\n## Babel Syntax Plugins\nWe have decided to only use ECMAScript proposals that are in stage 3 (or later) (See [TC39 Proposals](https://github.com/tc39/proposals)). Additionally, because we are migrating to typescript, we will align with what their compiler supports.\nThe only exception to this are decorators.\n\n## New Syntax\n\n### Optional Chaining\n\n[Optional chaining](https://github.com/tc39/proposal-optional-chaining) helps us access [nested] objects without having to check for existence before each property/method access. If we try to access a property of an `undefined` or `null` object, it will stop and return `undefined`. \n\n#### Syntax\n\u003e The Optional Chaining operator is spelled `?.`. It may appear in three positions:\n```\nobj?.prop       // optional static property access\nobj?.[expr]     // optional dynamic property access\nfunc?.(...args) // optional function or method call\n```\n--- From https://github.com/tc39/proposal-optional-chaining\n\n### Nullish Coalescing\n\nThis is a way to set a \"default\" value. e.g. previously you would do something like\n\n    let x = volume || 0.5;\n\nWhich is a problem since `0` is a valid value for `volume`, but because it evaluates to `false` -y, we do not short circuit the expression and the value of `x` is `0.5`\n\nIf instead we used [nullish coalescing](https://github.com/tc39/proposal-nullish-coalescing)\n\n    let x = volume ?? 0.5\n\nIt will only default to `0.5` if `volume` is `null` or `undefined`.\n\n#### Syntax\n\u003e Base case. If the expression at the left-hand side of the ?? operator evaluates to undefined or null, its right-hand side is returned.\n```\nconst response = {\n  settings: {\n    nullValue: null,\n    height: 400,\n    animationDuration: 0,\n    headerText: '',\n    showSplashScreen: false\n  }\n};\n\nconst undefinedValue = response.settings.undefinedValue ?? 'some other default'; // result: 'some other default'\nconst nullValue = response.settings.nullValue ?? 'some other default'; // result: 'some other default'\nconst headerText = response.settings.headerText ?? 'Hello, world!'; // result: ''\nconst animationDuration = response.settings.animationDuration ?? 300; // result: 0\nconst showSplashScreen = response.settings.showSplashScreen ?? true; // result: false\n```\n--- From https://github.com/tc39/proposal-nullish-coalescing\n\n## Lodash\nBe sure to not import `lodash` utilities using the default `lodash` package. There is an `eslint` rule to make sure this does not happen. Instead, import the utility directly, e.g. `import isEqual from 'lodash/isEqual';`.\n\nPreviously we used a combination of [lodash-webpack-plugin](https://www.npmjs.com/package/lodash-webpack-plugin) and [babel-plugin-lodash](https://github.com/lodash/babel-plugin-lodash) but it is easy to overlook these plugins and configuration when trying to use a new lodash utility (e.g. [this PR](https://github.com/getsentry/sentry/pull/13834)). With `webpack` tree shaking and `eslint` enforcement, we should be able to maintain reasonable bundle sizes.\n\nSee [this PR](https://github.com/getsentry/sentry/pull/15521) for more information.\n\nWe prefer using optional chaining and nullish coalescing over `get` from `loadash/get`.\n\n## Typescript\n\n### Typing defaultProps in class components\n\n```javascript\nimport React from 'react';\n\ntype DefaultProps = {\n  size: 'Small' | 'Medium' | 'Large'; // these should not be marked as optional\n};\n\n// no Partial\u003cDefaultProps\u003e\ntype Props = DefaultProps \u0026 {\n  name: string;\n  codename?: string;\n};\n\nclass Planet extends React.Component\u003cProps\u003e {\n  // no Partial\u003cProps\u003e because it would mark everything as optional\n  static defaultProps: DefaultProps = {\n    size: 'Medium',\n  };\n\n  render() {\n    const {name, size, codename} = this.props;\n\n    return (\n      \u003cp\u003e\n        {name} is a {size.toLowerCase()} planet.\n        {codename \u0026\u0026 ` Its codename is ${codename}`}\n      \u003c/p\u003e\n    );\n  }\n}\n\nconst planet = \u003cPlanet name=\"Mars\" /\u003e;\n```\nor with help of typeof:\n```javascript\nimport React from 'react';\n\nconst defaultProps = {\n  size: 'Medium' as 'Small' | 'Medium' | 'Large',\n};\n\ntype Props = {\n  name: string;\n  codename?: string;\n} \u0026 typeof defaultProps;\n// no Partial\u003ctypeof defaultProps\u003e because it would mark everything as optional\n\nclass Planet extends React.Component\u003cProps\u003e {\n  static defaultProps = defaultProps;\n\n  render() {\n    const {name, size, codename} = this.props;\n\n    return (\n      \u003cp\u003e\n        {name} is a {size.toLowerCase()} planet. Its color is{' '}\n        {codename \u0026\u0026 ` Its codename is ${codename}`}\n      \u003c/p\u003e\n    );\n  }\n}\n\nconst planet = \u003cPlanet name=\"Mars\" /\u003e;\n```\n\n### Typing function components\n\n```javascript\nimport React from 'react';\n\n// defaultProps on function components are being discontinued in the future\n// https://twitter.com/dan_abramov/status/1133878326358171650\n// https://github.com/reactjs/rfcs/pull/107\n// we should probably use default params\n\ntype Props = {\n  name: string;\n  size?: 'Small' | 'Medium' | 'Large'; // props with es6 default params should be marked as optional\n  codename?: string;\n};\n\n// consensus is that typing destructured Props is slightly better than using React.FC\u003cProps\u003e\n// https://github.com/typescript-cheatsheets/react-typescript-cheatsheet#function-components\nconst Planet = ({name, size = 'Medium', codename}: Props) =\u003e {\n  return (\n    \u003cp\u003e\n      {name} is a {size.toLowerCase()} planet.\n      {codename \u0026\u0026 ` Its codename is ${codename}`}\n    \u003c/p\u003e\n  );\n};\n\nconst planet = \u003cPlanet name=\"Mars\" /\u003e;\n```\n\n## Contributing\n\nThe [issue tracker](https://github.com/getsentry/frontend-handbook/issues/) is the prefered channel to propose and disucss a change. Or create a [pull request](https://github.com/getsentry/frontend-handbook/pulls)!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetsentry%2Ffrontend-handbook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgetsentry%2Ffrontend-handbook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetsentry%2Ffrontend-handbook/lists"}