├── .changeset ├── README.md └── config.json ├── .eslintrc.cjs ├── .github ├── actions │ ├── build-test │ │ └── action.yml │ └── setup │ │ └── action.yml └── workflows │ ├── pull-request.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── customizing.md └── images │ ├── envy-custom-system.png │ ├── envy-example.png │ ├── envy-hero.png │ ├── envy-request-component.png │ ├── envy-response-component.png │ ├── envy-system-icon.png │ └── envy-trace-row-data.png ├── examples ├── apollo-client │ ├── .postcssrc │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── CatFact.tsx │ │ │ ├── Cocktail.tsx │ │ │ ├── Sanity.tsx │ │ │ └── Xkcd.tsx │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ ├── queries │ │ │ └── index.ts │ │ └── viewer │ │ │ ├── systems │ │ │ ├── CatFacts.tsx │ │ │ └── CocktailDb.tsx │ │ │ ├── viewer.html │ │ │ └── viewer.js │ ├── tailwind.config.js │ └── tsconfig.json ├── apollo │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── schema │ │ ├── catFacts │ │ │ ├── resolvers.ts │ │ │ └── schema.ts │ │ ├── cocktails │ │ │ ├── resolvers.ts │ │ │ └── schema.ts │ │ ├── sanity │ │ │ ├── queries.ts │ │ │ ├── resolvers.ts │ │ │ ├── schema.ts │ │ │ └── transformers.ts │ │ └── xkcd │ │ │ ├── resolvers.ts │ │ │ └── schema.ts │ │ └── utils │ │ └── sanityClient.ts ├── express-client │ ├── .eslintrc.cjs │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── CatFact.tsx │ │ │ ├── Cocktail.tsx │ │ │ └── RandomeDogImage.tsx │ │ ├── index.css │ │ ├── main.jsx │ │ └── utils │ │ │ ├── query.ts │ │ │ └── types.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── express │ ├── app.ts │ ├── package.json │ └── tsconfig.json └── next │ ├── .eslintrc.json │ ├── README.md │ ├── app │ ├── _components │ │ ├── CatFact.tsx │ │ ├── Cocktail.tsx │ │ ├── Dogo.tsx │ │ └── index.ts │ ├── api │ │ └── swapi │ │ │ └── route.ts │ ├── favicon.ico │ ├── layout.tsx │ ├── next13app │ │ ├── call-api-route │ │ │ ├── ApiRouteExample.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ └── page.tsx │ ├── globals.css │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ └── next13pages │ │ └── index.tsx │ ├── postcss.config.js │ ├── shared │ ├── CatFactContainer.tsx │ ├── CatFactWidget.tsx │ ├── CocktailContainer.tsx │ ├── CocktailWidget.tsx │ ├── DogContainer.tsx │ ├── DogWidget.tsx │ └── index.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── utils │ ├── query.ts │ └── types.ts ├── jest.config.ts ├── package.json ├── packages ├── apollo │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── consts.ts │ │ ├── event.ts │ │ ├── fetch.test.ts │ │ ├── fetch.ts │ │ ├── fetchTypes.ts │ │ ├── graphql.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── json.test.ts │ │ ├── json.ts │ │ ├── log.ts │ │ ├── middleware │ │ │ ├── graphql.test.ts │ │ │ ├── graphql.ts │ │ │ ├── index.ts │ │ │ ├── meta.test.ts │ │ │ ├── meta.ts │ │ │ ├── sanity.test.ts │ │ │ └── sanity.ts │ │ ├── nanoid.ts │ │ ├── options.ts │ │ ├── payload.ts │ │ ├── plugin.ts │ │ ├── sanity.ts │ │ ├── time.ts │ │ ├── url.test.ts │ │ ├── url.ts │ │ └── websocket.ts │ └── tsconfig.json ├── nextjs │ ├── CHANGELOG.md │ ├── README.md │ ├── globals.d.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── loaders │ │ │ ├── config.loader.ts │ │ │ ├── page.loader.ts │ │ │ ├── server.loader.ts │ │ │ └── web.loader.ts │ │ ├── route.ts │ │ ├── tracing.ts │ │ └── webpack.ts │ └── tsconfig.json ├── node │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── fetch.ts │ │ ├── http.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── test.ts │ │ ├── tracing.ts │ │ └── utils │ │ │ ├── time.ts │ │ │ └── wrap.ts │ └── tsconfig.json ├── web │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── fetch.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── performance.ts │ │ └── tracing.ts │ └── tsconfig.json └── webui │ ├── .eslintrc.cjs │ ├── .parcelrc │ ├── .postcssrc │ ├── .storybook │ ├── addons │ │ └── preset.ts │ ├── main.ts │ └── preview.ts │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── pkg │ └── demoResolver.js │ ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── collector │ │ ├── CollectorClient.test.ts │ │ ├── CollectorClient.ts │ │ └── __storybook__mocks__ │ │ │ └── CollectorClient.ts │ ├── components │ │ ├── Authorization.stories.tsx │ │ ├── Authorization.test.tsx │ │ ├── Authorization.tsx │ │ ├── Badge.stories.ts │ │ ├── Badge.test.tsx │ │ ├── Badge.tsx │ │ ├── Button.stories.ts │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ ├── Code.stories.ts │ │ ├── Code.test.tsx │ │ ├── Code.tsx │ │ ├── CodeDisplay.test.tsx │ │ ├── CodeDisplay.tsx │ │ ├── DarkModeToggle.stories.tsx │ │ ├── DarkModeToggle.test.tsx │ │ ├── DarkModeToggle.tsx │ │ ├── DateTime.stories.tsx │ │ ├── DateTime.test.tsx │ │ ├── DateTime.tsx │ │ ├── Fields.stories.tsx │ │ ├── Fields.test.tsx │ │ ├── Fields.tsx │ │ ├── Input.stories.ts │ │ ├── Input.test.tsx │ │ ├── Input.tsx │ │ ├── KeyValueList.stories.ts │ │ ├── KeyValueList.test.tsx │ │ ├── KeyValueList.tsx │ │ ├── Loading.stories.ts │ │ ├── Loading.test.tsx │ │ ├── Loading.tsx │ │ ├── Menu.stories.ts │ │ ├── Menu.test.tsx │ │ ├── Menu.tsx │ │ ├── MonacoEditor.tsx │ │ ├── SearchInput.stories.ts │ │ ├── SearchInput.test.tsx │ │ ├── SearchInput.tsx │ │ ├── Section.stories.ts │ │ ├── Section.test.tsx │ │ ├── Section.tsx │ │ ├── ToggleSwitch.stories.ts │ │ ├── ToggleSwitch.test.tsx │ │ ├── ToggleSwitch.tsx │ │ ├── index.ts │ │ └── ui │ │ │ ├── CopyAsCurlButton.stories.tsx │ │ │ ├── CopyAsCurlButton.test.tsx │ │ │ ├── CopyAsCurlButton.tsx │ │ │ ├── DebugToolbar.stories.tsx │ │ │ ├── DebugToolbar.test.tsx │ │ │ ├── DebugToolbar.tsx │ │ │ ├── FiltersAndActions.stories.tsx │ │ │ ├── FiltersAndActions.test.tsx │ │ │ ├── FiltersAndActions.tsx │ │ │ ├── Header.stories.tsx │ │ │ ├── Header.test.tsx │ │ │ ├── Header.tsx │ │ │ ├── Logo.tsx │ │ │ ├── MainDisplay.stories.tsx │ │ │ ├── MainDisplay.test.tsx │ │ │ ├── MainDisplay.tsx │ │ │ ├── QueryParams.stories.tsx │ │ │ ├── QueryParams.test.tsx │ │ │ ├── QueryParams.tsx │ │ │ ├── RequestHeaders.stories.tsx │ │ │ ├── RequestHeaders.test.tsx │ │ │ ├── RequestHeaders.tsx │ │ │ ├── ResponseHeaders.stories.tsx │ │ │ ├── ResponseHeaders.test.tsx │ │ │ ├── ResponseHeaders.tsx │ │ │ ├── SourceAndSystemFilter.stories.tsx │ │ │ ├── SourceAndSystemFilter.test.tsx │ │ │ ├── SourceAndSystemFilter.tsx │ │ │ ├── Tabs.stories.tsx │ │ │ ├── Tabs.test.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── TimingsDiagram.stories.tsx │ │ │ ├── TimingsDiagram.test.tsx │ │ │ ├── TimingsDiagram.tsx │ │ │ ├── TraceDetail.stories.tsx │ │ │ ├── TraceDetail.test.tsx │ │ │ ├── TraceDetail.tsx │ │ │ ├── TraceList.stories.tsx │ │ │ ├── TraceList.test.tsx │ │ │ ├── TraceList.tsx │ │ │ ├── TraceListHeader.test.tsx │ │ │ ├── TraceListHeader.tsx │ │ │ ├── TraceListPlaceholder.test.tsx │ │ │ ├── TraceListPlaceholder.tsx │ │ │ ├── TraceListRow.test.tsx │ │ │ ├── TraceListRow.tsx │ │ │ ├── TraceListRowCell.test.tsx │ │ │ ├── TraceListRowCell.tsx │ │ │ ├── TraceRequestData.stories.tsx │ │ │ ├── TraceRequestData.test.tsx │ │ │ └── TraceRequestData.tsx │ ├── context │ │ ├── ApplicationContext.test.tsx │ │ └── ApplicationContext.tsx │ ├── favicon.png │ ├── hooks │ │ ├── useApplication.test.ts │ │ ├── useApplication.ts │ │ ├── useClickAway.test.tsx │ │ ├── useClickAway.ts │ │ ├── useFeatureFlags.ts │ │ ├── useKeyboardShortcut.test.ts │ │ ├── useKeyboardShortcut.ts │ │ ├── usePlatform.test.ts │ │ └── usePlatform.ts │ ├── index.html │ ├── index.js │ ├── integration.tsx │ ├── scripts │ │ ├── buildIntegration.cjs │ │ ├── start.cjs │ │ ├── startCollector.cjs │ │ ├── startViewer.cjs │ │ └── startViewerDev.cjs │ ├── styles │ │ ├── allotment.css │ │ └── base.css │ ├── systems │ │ ├── Default.test.tsx │ │ ├── Default.tsx │ │ ├── GraphQL.test.tsx │ │ ├── GraphQL.tsx │ │ ├── Sanity.test.tsx │ │ ├── Sanity.tsx │ │ ├── index.tsx │ │ ├── registration.test.ts │ │ ├── registration.ts │ │ └── systems.test.tsx │ ├── testing │ │ ├── mockSystems.ts │ │ ├── mockTraces │ │ │ ├── gql.ts │ │ │ ├── index.ts │ │ │ ├── large-gql.ts │ │ │ ├── rest.ts │ │ │ ├── sanity.ts │ │ │ ├── util.ts │ │ │ └── xml.ts │ │ ├── mockUseApplication.ts │ │ ├── mockUsePlatform.ts │ │ ├── setupJest.ts │ │ ├── setupJestAfterEnv.ts │ │ └── setupJestGlobal.ts │ ├── types │ │ ├── graphql-prettier.d.ts │ │ └── index.ts │ └── utils │ │ ├── index.ts │ │ ├── styles.test.ts │ │ ├── styles.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.types.json │ └── vite.config.ts ├── scripts └── test-webui-npm.sh ├── tsconfig.json ├── turbo.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | 10 | ## Releases 11 | 12 | Releases are automatically managed by the CI, and changesets are added to each PR before merge. 13 | 14 | ### Adding a changeset 15 | 16 | The following command will create a changeset in your Pull Request 17 | 18 | ``` 19 | yarn changeset 20 | ``` 21 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@envyjs/apollo", "@envyjs/core", "@envyjs/nextjs", "@envyjs/node", "@envyjs/web", "@envyjs/webui"]], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "@envyjs/example-apollo", 12 | "@envyjs/example-apollo-client", 13 | "@envyjs/example-express", 14 | "@envyjs/example-express-client", 15 | "@envyjs/example-next" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/actions/build-test/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Lint Test 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Build 7 | run: yarn build --cache-dir=".turbo" 8 | shell: bash 9 | env: 10 | NODE_ENV: production 11 | 12 | - name: Lint 13 | run: yarn lint --cache-dir=".turbo" 14 | shell: bash 15 | 16 | - name: Test 17 | run: yarn test --cache-dir=".turbo" 18 | shell: bash 19 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Build Step 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Setup Node 8 | uses: actions/setup-node@v4 9 | with: 10 | node-version-file: '.nvmrc' 11 | cache: 'yarn' 12 | 13 | - name: Install dependencies 14 | shell: bash 15 | run: yarn install --frozen-lockfile 16 | 17 | - name: Turbo Cache 18 | uses: actions/cache@v4 19 | with: 20 | path: .turbo 21 | key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }} 22 | restore-keys: | 23 | turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }} 24 | turbo-${{ github.job }}-${{ github.ref_name }}- 25 | turbo-${{ github.job }}- 26 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: ./.github/actions/setup 11 | - uses: ./.github/actions/build-test 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: ./.github/actions/setup 15 | - uses: ./.github/actions/build-test 16 | 17 | publish: 18 | runs-on: ubuntu-latest 19 | needs: [build] 20 | permissions: 21 | contents: write 22 | id-token: write 23 | issues: write 24 | repository-projects: write 25 | deployments: write 26 | packages: write 27 | pull-requests: write 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ./.github/actions/setup 32 | 33 | - name: Build 34 | run: yarn build --cache-dir=".turbo" 35 | env: 36 | NODE_ENV: production 37 | 38 | - uses: changesets/action@v1 39 | with: 40 | version: yarn run changeset version 41 | publish: yarn run changeset publish 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | NPM_CONFIG_PROVENANCE: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | *.lcov 16 | .nyc_output 17 | 18 | # Dependency directories 19 | node_modules/ 20 | jspm_packages/ 21 | 22 | # TypeScript cache 23 | *.tsbuildinfo 24 | 25 | # Optional npm cache directory 26 | .npm 27 | 28 | # Optional eslint cache 29 | .eslintcache 30 | 31 | # Optional stylelint cache 32 | .stylelintcache 33 | 34 | # Output of 'npm pack' 35 | *.tgz 36 | 37 | # Yarn Integrity file 38 | .yarn-integrity 39 | 40 | # build output 41 | .turbo 42 | dist 43 | bin 44 | .vscode 45 | .parcel-cache 46 | 47 | # yarn v2 48 | .yarn/cache 49 | .yarn/unplugged 50 | .yarn/build-state.yml 51 | .yarn/install-state.gz 52 | .pnp.* 53 | 54 | # examples/next.js 55 | **/.next/ 56 | **/next-env.d.ts 57 | 58 | # other 59 | .DS_Store 60 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ignore artifacts 2 | dist 3 | .turbo 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 120, 4 | "quoteProps": "consistent", 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "useTabs": false 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Formidable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/images/envy-custom-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-custom-system.png -------------------------------------------------------------------------------- /docs/images/envy-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-example.png -------------------------------------------------------------------------------- /docs/images/envy-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-hero.png -------------------------------------------------------------------------------- /docs/images/envy-request-component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-request-component.png -------------------------------------------------------------------------------- /docs/images/envy-response-component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-response-component.png -------------------------------------------------------------------------------- /docs/images/envy-system-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-system-icon.png -------------------------------------------------------------------------------- /docs/images/envy-trace-row-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/docs/images/envy-trace-row-data.png -------------------------------------------------------------------------------- /examples/apollo-client/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/apollo-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/example-apollo-client", 3 | "version": "1.0.0", 4 | "description": "Example website connected to apollo server application using Envy", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "start": "yarn start:web", 9 | "start:custom-viewer": "concurrently \"yarn start:envy\" \"yarn start:web\" \"yarn start:viewer\"", 10 | "start:web": "parcel ./src/index.html --port 4001 --no-cache", 11 | "start:envy": "npx @envyjs/webui --no-ui", 12 | "start:viewer": "parcel ./src/viewer/viewer.html --port 4002 --no-cache" 13 | }, 14 | "dependencies": { 15 | "@envyjs/web": "*", 16 | "@envyjs/webui": "*", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "urql": "^4.0.5" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.21", 23 | "@types/react-dom": "^18.2.7", 24 | "parcel": "^2.9.3", 25 | "postcss": "^8.4.29", 26 | "tailwindcss": "^3.3.3", 27 | "typescript": "^5.2.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/apollo-client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; 2 | 3 | import CatFact from './components/CatFact'; 4 | import Cocktail from './components/Cocktail'; 5 | import Sanity from './components/Sanity'; 6 | import Xkcd from './components/Xkcd'; 7 | 8 | const client = new Client({ 9 | url: 'http://localhost:4000/graphql', 10 | exchanges: [cacheExchange, fetchExchange], 11 | }); 12 | 13 | export function App() { 14 | return ( 15 | 16 |
17 |

Envy - Example website with Apollo GraphQL server

18 |

19 | This website will make calls to the example apollo GraphQL server, which will send request telemetry over 20 | websockets for Envy to display in one of the Network Viewer UIs. 21 |

22 |
23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /examples/apollo-client/src/components/CatFact.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | 3 | import { RANDOM_CAT_FACT_QUERY } from '../queries'; 4 | 5 | export default function CatFact() { 6 | const [result, refresh] = useQuery({ 7 | query: RANDOM_CAT_FACT_QUERY, 8 | requestPolicy: 'network-only', 9 | }); 10 | 11 | return ( 12 |
13 |

Random cat fact:

14 | {result.fetching ? ( 15 |

Loading cat fact...

16 | ) : ( 17 | (() => { 18 | const randomFact = result.data?.randomCatFact; 19 | if (!randomFact) return null; 20 | 21 | return ( 22 | <> 23 |

{randomFact.fact}

24 | 25 | ); 26 | })() 27 | )} 28 |
29 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/apollo-client/src/components/Cocktail.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | 3 | import { RANDOM_COCKTAIL_QUERY } from '../queries'; 4 | 5 | export default function Cocktail() { 6 | const [result, refresh] = useQuery({ 7 | query: RANDOM_COCKTAIL_QUERY, 8 | requestPolicy: 'network-only', 9 | }); 10 | 11 | return ( 12 |
13 |

Random cocktail:

14 | {result.fetching ? ( 15 |

Loading cocktail...

16 | ) : ( 17 | (() => { 18 | const cocktail = result.data?.randomCocktail; 19 | if (!cocktail) return null; 20 | 21 | return ( 22 |
23 |

{cocktail.name}

24 |
25 | 26 |
27 |

Ingredients

28 |
    29 | {cocktail.ingredients.map((x: string, idx: number) => ( 30 |
  • {x}
  • 31 | ))} 32 |
33 |
34 |
35 |
36 |

Instructions

37 |

{cocktail.instructions}

38 |
39 |
40 | ); 41 | })() 42 | )} 43 |
44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /examples/apollo-client/src/components/Sanity.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | 3 | import { CATEGORIES_WITH_PRODUCTS_QUERY } from '../queries'; 4 | 5 | export default function Sanity() { 6 | const [result, refresh] = useQuery({ 7 | query: CATEGORIES_WITH_PRODUCTS_QUERY, 8 | requestPolicy: 'network-only', 9 | }); 10 | 11 | return ( 12 |
13 |

Formidable Boulangerie products:

14 | {result.fetching ? ( 15 |

Loading categories and products...

16 | ) : ( 17 | (() => { 18 | const categoriesWithProducts = result.data?.categoriesWithProducts; 19 | if (!categoriesWithProducts) return null; 20 | 21 | return ( 22 |
23 | {categoriesWithProducts.map((category: any) => ( 24 |
25 |

{category.name}

26 |
27 | {category.products.map((product: any) => { 28 | return product.variants.map((variant: any) => ( 29 |
30 |
{variant.name}
31 |
${variant.price.toFixed(2)}
32 |
33 | )); 34 | })} 35 |
36 |
37 | ))} 38 |
39 | ); 40 | })() 41 | )} 42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/apollo-client/src/components/Xkcd.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | 3 | import { LATEST_XKCD_QUERY } from '../queries'; 4 | 5 | export default function Xkcd() { 6 | const [result, refresh] = useQuery({ 7 | query: LATEST_XKCD_QUERY, 8 | requestPolicy: 'network-only', 9 | }); 10 | 11 | return ( 12 |
13 |

Latest xkcd:

14 | {result.fetching ? ( 15 |

Loading comic...

16 | ) : ( 17 | (() => { 18 | const comic = result.data?.latestXkcd; 19 | if (!comic) return null; 20 | 21 | return ( 22 |
23 |

{comic.name}

24 | 25 |
26 | ); 27 | })() 28 | )} 29 |
30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/apollo-client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4 { 9 | @apply font-bold; 10 | } 11 | 12 | h1 { 13 | @apply text-3xl mb-8; 14 | } 15 | 16 | h2 { 17 | @apply text-2xl mb-4; 18 | } 19 | 20 | h3 { 21 | @apply text-xl mb-4; 22 | } 23 | 24 | h4 { 25 | @apply text-lg mt-4 mb-2; 26 | } 27 | 28 | ul { 29 | @apply list-disc ml-8; 30 | } 31 | 32 | button { 33 | @apply block bg-orange-500 text-white px-8 py-2 rounded mt-auto; 34 | } 35 | 36 | .content { 37 | @apply space-y-4; 38 | @apply xl:flex xl:flex-row xl:space-x-4 xl:space-y-0; 39 | } 40 | 41 | .thingy { 42 | @apply flex flex-col bg-slate-50 shadow-md p-4 rounded-lg xl:flex-1; 43 | } 44 | 45 | .button-container { 46 | @apply mt-auto pt-4; 47 | } 48 | -------------------------------------------------------------------------------- /examples/apollo-client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Envy - Example website (Apollo)( 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/apollo-client/src/index.js: -------------------------------------------------------------------------------- 1 | import { enableTracing } from '@envyjs/web'; 2 | enableTracing({ serviceName: 'examples/apollo-client' }); 3 | 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | import { App } from './App'; 7 | 8 | const container = document.getElementById('app'); 9 | const root = createRoot(container); 10 | root.render(); 11 | -------------------------------------------------------------------------------- /examples/apollo-client/src/queries/index.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'urql'; 2 | 3 | export const RANDOM_CAT_FACT_QUERY = gql` 4 | query RandomCatFact { 5 | randomCatFact { 6 | id 7 | fact 8 | } 9 | } 10 | `; 11 | 12 | export const RANDOM_COCKTAIL_QUERY = gql` 13 | query RandomCocktail { 14 | randomCocktail { 15 | id 16 | name 17 | ingredients 18 | instructions 19 | imageUrl 20 | } 21 | } 22 | `; 23 | 24 | export const LATEST_XKCD_QUERY = gql` 25 | query LatestXkcd { 26 | latestXkcd { 27 | id 28 | title 29 | imageUrl 30 | } 31 | } 32 | `; 33 | 34 | export const CATEGORIES_WITH_PRODUCTS_QUERY = gql` 35 | query CategoriesWithProducts { 36 | categoriesWithProducts { 37 | id 38 | name 39 | description 40 | products { 41 | id 42 | name 43 | description 44 | categoryIds 45 | variants { 46 | id 47 | name 48 | price 49 | } 50 | } 51 | } 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /examples/apollo-client/src/viewer/systems/CatFacts.tsx: -------------------------------------------------------------------------------- 1 | import { System, Trace } from '@envyjs/webui'; 2 | 3 | export default class CatFactsSystem implements System { 4 | name = 'Cat Facts API'; 5 | 6 | isMatch(trace: Trace) { 7 | return trace.http?.host === 'cat-fact.herokuapp.com'; 8 | } 9 | 10 | getIconUri() { 11 | return 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgaWQ9IkJsYWNrX2NhdCI+PHBhdGggZD0iTTM3MC4wOTE4LDUyLjE0NmMtNS4xMzEyLTUuMTAzNS0xMy40NS00LjczLTE4LjU2MzguMzg1Ni0yMi44NzQ4LDIyLjg1NzYtNDIuNDkzOSw1MC4zNTYzLTU3LjkwNDcsODEuMThhMjYyLjg1NDQsMjYyLjg1NDQsMCwwLDAtNzUuMzM2MywwLDMxMi45NTEyLDMxMi45NTEyLDAsMCwwLTU3LjgyMzUtODEuMTYwN2MtNS4xMS01LjEyMzgtMTMuNDMyNi01LjUwNTEtMTguNTY4MS0uNDAyN0M4My4zOTI2LDExMC4yODE0LDQ2LDE5OC4zMzc4LDQ2LDI5Ny4yNDg1YzAsOTEuODc1LDkzLjk3NzEsMTY2LjI1LDIxMCwxNjYuMjUsMTE1LjkzNzUsMCwyMTAtNzQuMzc1LDIxMC0xNjYuMjVDNDY2LDE5OC4zMzY4LDQyOC41MjYyLDExMC4yNzgxLDM3MC4wOTE4LDUyLjE0NlpNMTQ2LjYyNSwzMzAuODQ5M2MtMjQuMzI3NC00LjcyNTQtNDQuOTc2Mi0yMi4zMTI5LTU2Ljg3NS00Ni43MjU4LDExLjg5ODgtMjQuNDEyOCwzMi41NDc2LTQyLDU2Ljg3NS00Ni43MjQ3Wm0yNi4yNSwwVjIzNy4zOTg4YzI0LjIzNzcsNC43MjQzLDQ0Ljk3NjIsMjIuMzExOSw1Ni44NzUsNDYuNzI0N0MyMTcuODUxMiwzMDguNTM2NCwxOTcuMTEyNywzMjYuMTIzOSwxNzIuODc1LDMzMC44NDkzWm0xNjYuMjUsMGMtMjQuMzI3NC00LjcyNTQtNDQuOTc2Mi0yMi4zMTI5LTU2Ljg3NS00Ni43MjU4LDExLjg5ODgtMjQuNDEyOCwzMi41NDc2LTQyLDU2Ljg3NS00Ni43MjQ3Wm0yNi4yNSwwVjIzNy4zOTg4YzI0LjIzNzcsNC43MjQzLDQ0Ljk3NjIsMjIuMzExOSw1Ni44NzUsNDYuNzI0N0M0MTAuMzUxMiwzMDguNTM2NCwzODkuNjEyNywzMjYuMTIzOSwzNjUuMzc1LDMzMC44NDkzWiIvPjwvZz48L3N2Zz4='; 12 | } 13 | 14 | getTraceRowData() { 15 | return { 16 | data: 'Cat fact', 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/apollo-client/src/viewer/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Envy - Custom viewer 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/apollo-client/src/viewer/viewer.js: -------------------------------------------------------------------------------- 1 | import EnvyViewer from '@envyjs/webui'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import CatFactsSystem from './systems/CatFacts'; 5 | import CocktailDbSystem from './systems/CocktailDb'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | 10 | root.render(); 11 | -------------------------------------------------------------------------------- /examples/apollo-client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/apollo-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "react-jsx", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ES2022", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "strict": true, 11 | "target": "ES2022" 12 | }, 13 | "include": ["src/**/*", "index.js"] 14 | } 15 | -------------------------------------------------------------------------------- /examples/apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/example-apollo", 3 | "version": "1.0.0", 4 | "description": "Example Apollo server application using Envy", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "start": "ts-node ./src/index.ts" 9 | }, 10 | "dependencies": { 11 | "@apollo/server": "^4.9.3", 12 | "@graphql-tools/utils": "^10.0.6", 13 | "@sanity/client": "^4.0.1", 14 | "cors": "2.8.5", 15 | "graphql": "^16.8.0", 16 | "groqd": "^0.15.9", 17 | "node-fetch": "2" 18 | }, 19 | "devDependencies": { 20 | "@envyjs/node": "*", 21 | "@envyjs/webui": "*", 22 | "@types/cors": "2.8.14", 23 | "ts-node": "^10.9.1", 24 | "typescript": "^5.2.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/catFacts/resolvers.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export default { 4 | Query: { 5 | async randomCatFact() { 6 | const url = 'https://cat-fact.herokuapp.com/facts'; 7 | const resp = await fetch(url); 8 | const json = await resp.json(); 9 | 10 | const allFacts = json.map((x: any) => ({ 11 | id: x._id, 12 | fact: x.text, 13 | })); 14 | 15 | const randomIdx = Math.floor(Math.random() * allFacts.length); 16 | const randomFact = allFacts[randomIdx]; 17 | 18 | return randomFact; 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/catFacts/schema.ts: -------------------------------------------------------------------------------- 1 | export default `#graphql 2 | 3 | type CatFact { 4 | id: ID! 5 | fact: String! 6 | } 7 | 8 | extend type Query { 9 | randomCatFact: CatFact! 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/cocktails/resolvers.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export default { 4 | Query: { 5 | async randomCocktail() { 6 | const url = 'https://www.thecocktaildb.com/api/json/v1/1/random.php'; 7 | const resp = await fetch(url); 8 | const json = await resp.json(); 9 | const item = json.drinks[0]; 10 | 11 | const ingredients = []; 12 | for (let i = 1; i <= 15; i += 1) { 13 | const ingredient = item[`strIngredient${i}`]; 14 | if (!!ingredient) { 15 | ingredients.push(ingredient); 16 | } 17 | } 18 | 19 | const cocktail = { 20 | id: item.idDrink, 21 | name: item.strDrink, 22 | ingredients, 23 | instructions: item.strInstructions, 24 | imageUrl: item.strDrinkThumb, 25 | }; 26 | 27 | return cocktail; 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/cocktails/schema.ts: -------------------------------------------------------------------------------- 1 | export default `#graphql 2 | 3 | type Cocktail { 4 | id: ID! 5 | name: String! 6 | ingredients: [String!]! 7 | instructions: String! 8 | imageUrl: String 9 | } 10 | 11 | extend type Query { 12 | randomCocktail: Cocktail! 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/sanity/queries.ts: -------------------------------------------------------------------------------- 1 | import { q } from 'groqd'; 2 | 3 | import Sanity from '../../utils/sanityClient'; 4 | 5 | const categorySelection = { 6 | _id: q.string(), 7 | name: q.string(), 8 | description: q.string(), 9 | }; 10 | 11 | export function getAllCategories() { 12 | const client = new Sanity(); 13 | return client.runQuery(q('*').filterByType('category').grab$(categorySelection)); 14 | } 15 | 16 | const productsSelection = { 17 | _id: q.string(), 18 | name: q.string(), 19 | description: q.contentBlocks(), 20 | categories: q('categories').filter().deref().grab$({ _id: q.string() }), 21 | variants: q('variants').filter().deref().grab$({ 22 | _id: q.string(), 23 | name: q.string(), 24 | price: q.number(), 25 | }), 26 | }; 27 | 28 | export function getAllProducts() { 29 | const client = new Sanity(); 30 | return client.runQuery(q('*').filterByType('product').grab$(productsSelection)); 31 | } 32 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/sanity/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { getAllCategories, getAllProducts } from './queries'; 2 | import { transformCategoryData, transformProductData } from './transformers'; 3 | 4 | export default { 5 | Query: { 6 | async allCategories() { 7 | const categories = await getAllCategories(); 8 | return categories.map(transformCategoryData); 9 | }, 10 | 11 | async allProducts() { 12 | const products = await getAllProducts(); 13 | return products.map(transformProductData); 14 | }, 15 | 16 | async categoriesWithProducts() { 17 | const categories = await this.allCategories(); 18 | const products = await this.allProducts(); 19 | 20 | return categories.map((category: any) => ({ 21 | ...category, 22 | products: products.filter((product: any) => product.categoryIds?.includes(category.id)), 23 | })); 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/sanity/schema.ts: -------------------------------------------------------------------------------- 1 | export default `#graphql 2 | 3 | type Category { 4 | id: ID! 5 | name: String! 6 | description: String 7 | } 8 | 9 | type CategoryWithProduct { 10 | id: ID! 11 | name: String! 12 | description: String 13 | products: [Product!]! 14 | } 15 | 16 | type Product { 17 | id: ID! 18 | name: String! 19 | description: String 20 | categoryIds: [String!]! 21 | variants: [ProductVariant!]! 22 | } 23 | 24 | type ProductVariant { 25 | id: ID! 26 | name: String! 27 | price: Float! 28 | } 29 | 30 | extend type Query { 31 | allCategories: [Category!]! 32 | allProducts: [Product!]! 33 | categoriesWithProducts: [CategoryWithProduct!]! 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/sanity/transformers.ts: -------------------------------------------------------------------------------- 1 | export function transformCategoryData(sanityData: any) { 2 | return { 3 | id: sanityData._id, 4 | name: sanityData.name, 5 | description: sanityData.description, 6 | }; 7 | } 8 | 9 | export function transformProductData(sanityData: any) { 10 | return { 11 | id: sanityData._id, 12 | name: sanityData.name, 13 | description: sanityData.description?.[0]?.children?.join('\n\n') ?? '', 14 | categoryIds: sanityData.categories?.map((x: any) => x._id) ?? [], 15 | variants: 16 | sanityData.variants?.map((v: any) => ({ 17 | id: v._id, 18 | name: v.name, 19 | price: v.price, 20 | })) ?? [], 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/xkcd/resolvers.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export default { 4 | Query: { 5 | async latestXkcd() { 6 | const url = 'https://xkcd.com/info.0.json'; 7 | const resp = await fetch(url); 8 | const json = await resp.json(); 9 | const item = json; 10 | 11 | const xkcd = { 12 | id: Buffer.from(item.img).toString('base64'), 13 | title: item.title, 14 | imageUrl: item.img, 15 | }; 16 | 17 | return xkcd; 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/apollo/src/schema/xkcd/schema.ts: -------------------------------------------------------------------------------- 1 | export default `#graphql 2 | 3 | type XkdcComic { 4 | id: ID! 5 | title: String! 6 | imageUrl: String! 7 | } 8 | 9 | extend type Query { 10 | latestXkcd: XkdcComic! 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /examples/apollo/src/utils/sanityClient.ts: -------------------------------------------------------------------------------- 1 | import sanityClient, { SanityClient } from '@sanity/client'; 2 | import { BaseQuery, makeSafeQueryRunner } from 'groqd'; 3 | 4 | export default class Sanity { 5 | private client: SanityClient; 6 | 7 | constructor() { 8 | this.client = sanityClient({ 9 | apiVersion: '2021-10-21', 10 | projectId: '5bsv02jj', // "Formidable Boulangerie" Sanity project 11 | dataset: 'production', 12 | useCdn: true, 13 | useProjectHostname: true, 14 | }); 15 | } 16 | 17 | async runQuery(query: BaseQuery, params?: Record) { 18 | const runSafeQuery = makeSafeQueryRunner((query, params) => { 19 | return this.client.fetch(query, params); 20 | }); 21 | 22 | return runSafeQuery(query, params); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/express-client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /examples/express-client/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /examples/express-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Envy + Express 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/express-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/example-express-client", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.2.15", 16 | "@types/react-dom": "^18.2.7", 17 | "@vitejs/plugin-react": "^4.0.3", 18 | "autoprefixer": "^10.4.15", 19 | "postcss": "^8.4.29", 20 | "tailwindcss": "^3.3.3", 21 | "typescript": "^5.2.2", 22 | "vite": "^4.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/express-client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/express-client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import CatFact from './components/CatFact'; 2 | import Cocktail from './components/Cocktail'; 3 | import RandomDogImage from './components/RandomeDogImage'; 4 | 5 | function App() { 6 | return ( 7 |
8 |

Envy - Example website with Express server

9 |

10 | This website will make calls to the example express server, which will send request telemetry over websockets 11 | for Envy to display in one of the Network Viewer UIs. 12 |

13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /examples/express-client/src/components/CatFact.tsx: -------------------------------------------------------------------------------- 1 | import { fetchCatFact } from '../utils/query'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { CatFact as CatFactType } from '../utils/types'; 4 | 5 | export default function CatFact() { 6 | const [catFact, setCatFact] = useState(); 7 | 8 | const onRefresh = useCallback(() => fetchCatFact().then(setCatFact), []); 9 | 10 | useEffect(() => { 11 | onRefresh(); 12 | }, []); 13 | 14 | return ( 15 |
16 |

Random cat fact:

17 | {catFact &&

{catFact.text}

} 18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/express-client/src/components/Cocktail.tsx: -------------------------------------------------------------------------------- 1 | import { fetchRandomCocktail } from '../utils/query'; 2 | import { Cocktail as CocktailType } from '../utils/types'; 3 | import { useCallback, useEffect, useState } from 'react'; 4 | 5 | export default function Cocktail() { 6 | const [cocktail, setCocktail] = useState(); 7 | const onRefresh = useCallback(() => fetchRandomCocktail().then(setCocktail), []); 8 | 9 | useEffect(() => { 10 | onRefresh(); 11 | }, []); 12 | 13 | return ( 14 |
15 |

Random cocktail:

16 | {cocktail && ( 17 |
18 |

{cocktail.name}

19 |
20 | 21 |
22 |

Ingredients

23 |
    24 | {cocktail.ingredients.map((x: string, idx: number) => ( 25 |
  • {x}
  • 26 | ))} 27 |
28 |
29 |
30 |
31 |

Instructions

32 |

{cocktail.instructions}

33 |
34 |
35 | )} 36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/express-client/src/components/RandomeDogImage.tsx: -------------------------------------------------------------------------------- 1 | import { fetchRandomDog } from '../utils/query'; 2 | import { Dog as DogType } from '../utils/types'; 3 | import { useCallback, useEffect, useState } from 'react'; 4 | 5 | export default function RandomDogImage() { 6 | const [dog, setDog] = useState(); 7 | const onRefresh = useCallback(() => fetchRandomDog().then(setDog), []); 8 | 9 | useEffect(() => { 10 | onRefresh(); 11 | }, []); 12 | 13 | return ( 14 |
15 |

Ramdom Dog Image:

16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/express-client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4 { 9 | @apply font-bold; 10 | } 11 | 12 | h1 { 13 | @apply text-3xl mb-8; 14 | } 15 | 16 | h2 { 17 | @apply text-2xl mb-4; 18 | } 19 | 20 | h3 { 21 | @apply text-xl mb-4; 22 | } 23 | 24 | h4 { 25 | @apply text-lg mt-4 mb-2; 26 | } 27 | 28 | ul { 29 | @apply list-disc ml-8; 30 | } 31 | 32 | button { 33 | @apply block bg-orange-500 text-white px-8 py-2 rounded mt-auto; 34 | } 35 | 36 | .content { 37 | @apply space-y-4; 38 | @apply xl:flex xl:flex-row xl:space-x-4 xl:space-y-0; 39 | } 40 | 41 | .thingy { 42 | @apply flex flex-col bg-slate-50 shadow-md p-4 rounded-lg xl:flex-1; 43 | } 44 | 45 | .button-container { 46 | @apply mt-auto pt-4; 47 | } 48 | -------------------------------------------------------------------------------- /examples/express-client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render(); 7 | -------------------------------------------------------------------------------- /examples/express-client/src/utils/query.ts: -------------------------------------------------------------------------------- 1 | import { CatFact, Cocktail, Dog } from './types'; 2 | 3 | export async function fetchCatFact(): Promise { 4 | const res = await fetch('/api/cat-fact'); 5 | const { data } = await res.json(); 6 | return data; 7 | } 8 | 9 | export async function fetchRandomCocktail(): Promise { 10 | const res = await fetch('/api/cocktail'); 11 | const { data } = await res.json(); 12 | return data; 13 | } 14 | 15 | export async function fetchRandomDog(): Promise { 16 | const res = await fetch('/api/dog'); 17 | const { data } = await res.json(); 18 | return data; 19 | } 20 | -------------------------------------------------------------------------------- /examples/express-client/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type CatFact = { _id: string; text: string }; 2 | export type Cocktail = { 3 | id: string; 4 | name: string; 5 | ingredients: string[]; 6 | instructions: string[]; 7 | imageUrl: string; 8 | }; 9 | export type Dog = { 10 | imageUrl: string; 11 | }; 12 | -------------------------------------------------------------------------------- /examples/express-client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /examples/express-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "jsx": "react-jsx", 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ES2022", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "strict": true, 11 | "target": "ES2022" 12 | }, 13 | "include": ["src/**/*", "index.js"] 14 | } 15 | -------------------------------------------------------------------------------- /examples/express-client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: '4001', 9 | proxy: { 10 | '/api': { 11 | target: 'http://localhost:4000/', 12 | changeOrigin: true, 13 | } 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /examples/express/app.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/order 2 | import { enableTracing } from '@envyjs/node'; 3 | enableTracing({ serviceName: 'examples/express' }); 4 | import express, { NextFunction, Response } from 'express'; 5 | import fetch from 'node-fetch'; 6 | 7 | const app = express(); 8 | const port = 4000; 9 | 10 | app.use((_, response: Response, next: NextFunction) => { 11 | response.setHeader('Timing-Allow-Origin', '*'); 12 | next(); 13 | }); 14 | 15 | app.get('/api/cat-fact', async (_, response) => { 16 | const res = await fetch('https://cat-fact.herokuapp.com/facts'); 17 | const data = await res.json(); 18 | const allFacts = data.map((fact: { _id: string; text: string }) => ({ 19 | id: fact._id, 20 | text: fact.text, 21 | })); 22 | const randomIdx = Math.floor(Math.random() * allFacts.length); 23 | return response.send({ data: allFacts[randomIdx] }); 24 | }); 25 | 26 | app.get('/api/cocktail', async (_, response) => { 27 | const url = 'https://www.thecocktaildb.com/api/json/v1/1/random.php'; 28 | const resp = await fetch(url); 29 | const json = await resp.json(); 30 | const item = json.drinks[0]; 31 | 32 | const ingredients = []; 33 | for (let i = 1; i <= 15; i += 1) { 34 | const ingredient = item[`strIngredient${i}`]; 35 | if (ingredient) { 36 | ingredients.push(ingredient); 37 | } 38 | } 39 | 40 | return response.send({ 41 | data: { 42 | id: item.idDrink, 43 | name: item.strDrink, 44 | ingredients, 45 | instructions: item.strInstructions, 46 | imageUrl: item.strDrinkThumb, 47 | }, 48 | }); 49 | }); 50 | 51 | app.get('/api/dog', async (_, response) => { 52 | const url = 'https://dog.ceo/api/breeds/image/random'; 53 | const resp = await fetch(url); 54 | const item = await resp.json(); 55 | 56 | return response.send({ 57 | data: { 58 | imageUrl: item.message, 59 | }, 60 | }); 61 | }); 62 | 63 | // eslint-disable-next-line no-console 64 | app.listen(port, () => console.log(`Express Sever running on http://localhost:${port}`)); 65 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/example-express", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "ts-node ./app.ts" 7 | }, 8 | "license": "MIT", 9 | "dependencies": { 10 | "express": "^4.18.2", 11 | "@envyjs/node": "*" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "^4.17.17", 15 | "ts-node": "^10.9.1", 16 | "typescript": "^5.2.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/next/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/next/app/_components/CatFact.tsx: -------------------------------------------------------------------------------- 1 | import { CatFactWidget } from '@/shared'; 2 | import { fetchCatFact } from '@/utils/query'; 3 | 4 | export async function CatFact() { 5 | const fact = await fetchCatFact(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/next/app/_components/Cocktail.tsx: -------------------------------------------------------------------------------- 1 | import { CocktailWidget } from '@/shared'; 2 | import { fetchRandomCocktail } from '@/utils/query'; 3 | 4 | export async function Cocktail() { 5 | const cocktail = await fetchRandomCocktail(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/next/app/_components/Dogo.tsx: -------------------------------------------------------------------------------- 1 | import { DogWidget } from '@/shared'; 2 | import { fetchRandomDog } from '@/utils/query'; 3 | 4 | export async function Dogo() { 5 | const comic = await fetchRandomDog(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/next/app/_components/index.ts: -------------------------------------------------------------------------------- 1 | export { CatFact } from './CatFact'; 2 | export { Cocktail } from './Cocktail'; 3 | export { Dogo } from './Dogo'; 4 | -------------------------------------------------------------------------------- /examples/next/app/api/swapi/route.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | const SWAPI_URL = 'https://swapi.py4e.com/api/people/1'; 5 | 6 | export async function GET() { 7 | // Look - just one call to the SWAPI endpoint! 8 | const { data } = await axios.get(SWAPI_URL); 9 | return NextResponse.json(data); 10 | } 11 | -------------------------------------------------------------------------------- /examples/next/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/envy/ea1bb24cbfece740093868bf120c170b122c380a/examples/next/app/favicon.ico -------------------------------------------------------------------------------- /examples/next/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../globals.css'; 2 | import type { Metadata } from 'next'; 3 | import { Inter } from 'next/font/google'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | }; 11 | 12 | export default function RootLayout({ children }: { children: React.ReactNode }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/next/app/next13app/call-api-route/ApiRouteExample.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | const API_ROUTE_URL = 'http://localhost:3000/api/swapi'; 4 | const SWAPI_URL = 'https://swapi.dev/api/people/1'; 5 | 6 | export default function ApiRouteExample() { 7 | async function makeSwapiCall() { 8 | await fetch(API_ROUTE_URL); 9 | } 10 | 11 | const swapiUrl = {SWAPI_URL}; 12 | const apiRouteUrl = ( 13 | {API_ROUTE_URL} 14 | ); 15 | 16 | const button = ( 17 | 20 | ); 21 | 22 | return ( 23 |
24 |
25 |

Calling an API route which makes an upstream request

26 |

Click the button to make a call to an API route from this page.

27 |

28 | This will call {apiRouteUrl}, where that API route will call {swapiUrl} 29 |

30 |
31 |
{button}
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/next/app/next13app/call-api-route/page.tsx: -------------------------------------------------------------------------------- 1 | import ApiRouteExample from './ApiRouteExample'; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/next/app/next13app/page.tsx: -------------------------------------------------------------------------------- 1 | import { CatFact, Cocktail, Dogo } from '@/app/_components'; 2 | import { CatFactContainer, CocktailContainer, DogContainer } from '@/shared'; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

Envy - Example website with Next JS

9 |

10 | This website will make calls to a few endpoints using server render components, which will send request 11 | telemetry over websockets for Envy to display in one of the Network Viewer UIs. 12 |

13 |
14 |
15 |

Server Components

16 |
17 | 18 | 19 | 20 |
21 |

Client Components

22 |
23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/next/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 |

Envy - Example website with Next JS

8 |

9 | This website will make calls to a few endpoints using server render components, which will send request 10 | telemetry over websockets for Envy to display in one of the Network Viewer UIs. 11 |

12 |
13 |
14 |
15 |
16 | 21 | App Router 22 |
23 | Example 24 | 25 |
26 |
27 | 32 | Pages Router 33 |
34 | Example 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/next/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4 { 9 | @apply font-bold; 10 | } 11 | 12 | h1 { 13 | @apply text-3xl mb-8; 14 | } 15 | 16 | h2 { 17 | @apply text-2xl mb-4; 18 | } 19 | 20 | h3 { 21 | @apply text-xl mb-4; 22 | } 23 | 24 | h4 { 25 | @apply text-lg mt-4 mb-2; 26 | } 27 | 28 | ul { 29 | @apply list-disc ml-8; 30 | } 31 | 32 | button { 33 | @apply block bg-orange-500 text-white px-8 py-2 rounded mt-auto; 34 | } 35 | 36 | .content { 37 | @apply space-y-4; 38 | @apply xl:flex xl:flex-row xl:space-x-4 xl:space-y-0; 39 | } 40 | 41 | .thingy { 42 | @apply flex flex-col bg-slate-50 shadow-md p-4 rounded-lg xl:flex-1; 43 | } 44 | 45 | .button-container { 46 | @apply mt-auto pt-4; 47 | } 48 | -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | const { withEnvy } = require('@envyjs/nextjs'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = {}; 5 | 6 | const envyConfig = { 7 | serviceName: 'next-app', 8 | }; 9 | 10 | module.exports = withEnvy(nextConfig, envyConfig); 11 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/example-next", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start -p 4000", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/react": "18.2.21", 13 | "@types/react-dom": "18.2.7", 14 | "autoprefixer": "10.4.15", 15 | "axios": "1.6.5", 16 | "eslint": "8.49.0", 17 | "eslint-config-next": "13.4.19", 18 | "next": "13.4.19", 19 | "postcss": "8.4.29", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "tailwindcss": "3.3.3", 23 | "typescript": "5.2.2" 24 | }, 25 | "devDependencies": { 26 | "@envyjs/nextjs": "*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | import '../globals.css'; 4 | 5 | export default function MyApp({ Component, pageProps }: AppProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/next/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/next/pages/next13pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetchCatFact, fetchRandomCocktail, fetchRandomDog } from '@/utils/query'; 2 | import { CatFact, Cocktail, Dog } from '@/utils/types'; 3 | import { CatFactContainer, CocktailContainer, DogContainer } from '@/shared'; 4 | 5 | type Props = { fact: CatFact; cocktail: Cocktail; dog: Dog }; 6 | 7 | export default function Home({ fact, cocktail, dog }: Props) { 8 | return ( 9 |
10 |
11 |

Envy - Example website with Next JS

12 |

13 | This website will make calls to a few endpoints using server render components, which will send request 14 | telemetry over websockets for Envy to display in one of the Network Viewer UIs. 15 |

16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | 27 | export async function getStaticProps() { 28 | const fact = await fetchCatFact(); 29 | const dog = await fetchRandomDog(); 30 | const cocktail = await fetchRandomCocktail(); 31 | 32 | return { 33 | props: { 34 | fact, 35 | cocktail, 36 | dog, 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /examples/next/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/next/shared/CatFactContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CatFactWidget } from '@/shared'; 4 | import { fetchCatFact } from '@/utils/query'; 5 | import { CatFact } from '@/utils/types'; 6 | import { useCallback, useEffect, useState } from 'react'; 7 | 8 | type Props = { 9 | initialCatFact?: CatFact; 10 | }; 11 | 12 | export function CatFactContainer({ initialCatFact }: Props) { 13 | const [catFact, setCatFact] = useState(initialCatFact); 14 | 15 | const onRefresh = useCallback(() => fetchCatFact().then(setCatFact), []); 16 | 17 | useEffect(() => { 18 | if (!initialCatFact) { 19 | onRefresh(); 20 | } 21 | }, []); 22 | 23 | return ( 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/next/shared/CatFactWidget.tsx: -------------------------------------------------------------------------------- 1 | import { CatFact } from '@/utils/types'; 2 | 3 | type Props = { 4 | fact?: CatFact; 5 | children?: React.ReactNode; 6 | }; 7 | 8 | export function CatFactWidget({ fact, children }: Props) { 9 | return ( 10 |
11 |

Random cat fact:

12 |

{fact?.text}

13 | {children} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/next/shared/CocktailContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CocktailWidget } from '@/shared'; 4 | import { fetchRandomCocktail } from '@/utils/query'; 5 | import { Cocktail } from '@/utils/types'; 6 | import { useCallback, useEffect, useState } from 'react'; 7 | 8 | type Props = { 9 | initialCocktail?: Cocktail; 10 | }; 11 | 12 | export function CocktailContainer({ initialCocktail }: Props) { 13 | const [cocktail, setCocktail] = useState(initialCocktail); 14 | 15 | const onRefresh = useCallback(() => fetchRandomCocktail().then(setCocktail), []); 16 | 17 | useEffect(() => { 18 | if (!initialCocktail) { 19 | onRefresh(); 20 | } 21 | }, []); 22 | 23 | return ( 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/next/shared/CocktailWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Cocktail } from '@/utils/types'; 2 | 3 | type Props = { 4 | cocktail?: Cocktail; 5 | children?: React.ReactNode; 6 | }; 7 | 8 | export function CocktailWidget({ cocktail, children }: Props) { 9 | return ( 10 |
11 |

Random cocktail:

12 | {cocktail && ( 13 |
14 |

{cocktail.name}

15 |
16 | 17 |
18 |

Ingredients

19 |
    20 | {cocktail.ingredients.map((x: string, idx: number) => ( 21 |
  • {x}
  • 22 | ))} 23 |
24 |
25 |
26 |
27 |

Instructions

28 |

{cocktail.instructions}

29 |
30 |
31 | )} 32 | {children} 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/next/shared/DogContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DogWidget } from '@/shared'; 4 | import { fetchRandomDog } from '@/utils/query'; 5 | import { Dog } from '@/utils/types'; 6 | import { useCallback, useEffect, useState } from 'react'; 7 | 8 | type Props = { 9 | initialDog?: Dog; 10 | }; 11 | 12 | export function DogContainer({ initialDog }: Props) { 13 | const [dog, setDog] = useState(initialDog); 14 | 15 | const onRefresh = useCallback(() => fetchRandomDog().then(setDog), []); 16 | 17 | useEffect(() => { 18 | if (!initialDog) { 19 | onRefresh(); 20 | } 21 | }, []); 22 | 23 | return ( 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/next/shared/DogWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Dog } from '@/utils/types'; 2 | 3 | type Props = { 4 | dogo?: Dog; 5 | children?: React.ReactNode; 6 | }; 7 | 8 | export function DogWidget({ dogo, children }: Props) { 9 | return ( 10 |
11 |

Random Dog Image:

12 |
13 | 14 |
15 | {children} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/next/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CatFactWidget'; 2 | export * from './CocktailWidget'; 3 | export * from './DogWidget'; 4 | export * from './CatFactContainer'; 5 | export * from './CocktailContainer'; 6 | export * from './DogContainer'; 7 | -------------------------------------------------------------------------------- /examples/next/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /examples/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["./*"], 24 | "@/components": ["app/_components"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /examples/next/utils/query.ts: -------------------------------------------------------------------------------- 1 | import { CatFact, Cocktail, Dog } from './types'; 2 | 3 | export async function fetchCatFact(): Promise { 4 | const res = await fetch('https://catfact.ninja/fact'); 5 | const data = await res.json(); 6 | return { 7 | text: data.fact, 8 | }; 9 | } 10 | 11 | export async function fetchRandomCocktail(): Promise { 12 | const url = 'https://www.thecocktaildb.com/api/json/v1/1/random.php'; 13 | const resp = await fetch(url); 14 | const json = await resp.json(); 15 | const item = json.drinks[0]; 16 | 17 | const ingredients = []; 18 | for (let i = 1; i <= 15; i += 1) { 19 | const ingredient = item[`strIngredient${i}`]; 20 | if (ingredient) { 21 | ingredients.push(ingredient); 22 | } 23 | } 24 | 25 | return { 26 | id: item.idDrink, 27 | name: item.strDrink, 28 | ingredients, 29 | instructions: item.strInstructions, 30 | imageUrl: item.strDrinkThumb, 31 | }; 32 | } 33 | 34 | export async function fetchRandomDog(): Promise { 35 | const url = 'https://dog.ceo/api/breeds/image/random'; 36 | const resp = await fetch(url); 37 | const item = await resp.json(); 38 | 39 | return { 40 | imageUrl: item.message, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /examples/next/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type CatFact = { text: string }; 2 | export type Cocktail = { 3 | id: string; 4 | name: string; 5 | ingredients: string[]; 6 | instructions: string[]; 7 | imageUrl: string; 8 | }; 9 | export type Dog = { 10 | imageUrl: string; 11 | }; 12 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from 'ts-jest'; 2 | 3 | const jestConfig: JestConfigWithTsJest = { 4 | preset: 'ts-jest', 5 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/src/**/?(*.)+(spec|test).[jt]s?(x)'], 6 | }; 7 | 8 | export default jestConfig; 9 | -------------------------------------------------------------------------------- /packages/apollo/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/apollo", 3 | "version": "0.10.1", 4 | "description": "Node.js Network & Telemetry Viewer", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "homepage": "https://github.com/formidablelabs/envy", 11 | "keywords": [ 12 | "react", 13 | "nextjs", 14 | "graphql", 15 | "typescript", 16 | "nodejs", 17 | "telemetry", 18 | "tracing" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/FormidableLabs/envy.git", 23 | "directory": "packages/apollo" 24 | }, 25 | "license": "MIT", 26 | "publishConfig": { 27 | "provenance": true 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "prebuild": "rimraf dist", 35 | "build": "tsc" 36 | }, 37 | "dependencies": { 38 | "@envyjs/node": "0.10.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/apollo/src/index.ts: -------------------------------------------------------------------------------- 1 | export const one = 1; 2 | -------------------------------------------------------------------------------- /packages/apollo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["**/*.spec.ts"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/core/jest.config.ts: -------------------------------------------------------------------------------- 1 | import rootConfig from '../../jest.config'; 2 | 3 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 4 | export default { 5 | ...rootConfig, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/core", 3 | "version": "0.10.1", 4 | "description": "Node.js Network & Telemetry Viewer", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "homepage": "https://github.com/formidablelabs/envy", 11 | "keywords": [ 12 | "react", 13 | "nextjs", 14 | "graphql", 15 | "typescript", 16 | "nodejs", 17 | "telemetry", 18 | "tracing" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/FormidableLabs/envy.git", 23 | "directory": "packages/core" 24 | }, 25 | "license": "MIT", 26 | "publishConfig": { 27 | "provenance": true 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "prebuild": "rimraf dist", 35 | "build": "tsc", 36 | "test": "jest" 37 | }, 38 | "dependencies": { 39 | "isomorphic-ws": "^5.0.0" 40 | }, 41 | "devDependencies": { 42 | "@types/ws": "8.5.7", 43 | "ws": "8.14.2" 44 | }, 45 | "optionalDependencies": { 46 | "ws": "8.14.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_WEB_SOCKET_PORT = 9999; 2 | -------------------------------------------------------------------------------- /packages/core/src/event.ts: -------------------------------------------------------------------------------- 1 | import { GraphqlRequest } from './graphql'; 2 | import { HttpRequest } from './http'; 3 | import { SanityRequest } from './sanity'; 4 | 5 | /** 6 | * An event that can be emitted through the websocket 7 | * @private This is an internal type and should not be used by consumers 8 | */ 9 | export interface Event { 10 | /** 11 | * A unique identifier for this span 12 | */ 13 | id: string; 14 | 15 | /** 16 | * UNIX Epoch time in seconds since 00:00:00 UTC on 1 January 1970 17 | */ 18 | timestamp: number; 19 | 20 | /** 21 | * A unique identifier used for grouping 22 | * multiple events 23 | */ 24 | parentId?: string; 25 | 26 | /** 27 | * Optional service name identifier 28 | */ 29 | serviceName?: string; 30 | 31 | /** 32 | * Graphql request data 33 | */ 34 | graphql?: GraphqlRequest; 35 | 36 | /** 37 | * Http request data 38 | */ 39 | http?: HttpRequest; 40 | 41 | /** 42 | * Sanity request data 43 | */ 44 | sanity?: SanityRequest; 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/fetchTypes.ts: -------------------------------------------------------------------------------- 1 | // These types are subsets of the libDom, 2 | // for cross-compatibility with Node 17+ and browser 3 | interface Headers { 4 | entries(): IterableIterator<[string, string]>; 5 | } 6 | 7 | export type HeadersInit = [string, string][] | Record | Headers; 8 | 9 | interface Request { 10 | readonly headers: Headers; 11 | readonly method: string; 12 | readonly url: string; 13 | } 14 | 15 | export type RequestInfo = Request | string; 16 | 17 | type BodyInit = ReadableStream | XMLHttpRequestBodyInit; 18 | 19 | export interface RequestInit { 20 | body?: BodyInit | null; 21 | headers?: HeadersInit; 22 | method?: string; 23 | } 24 | 25 | export interface Response { 26 | readonly headers: Headers; 27 | readonly status: number; 28 | readonly statusText: string; 29 | readonly type: ResponseType; 30 | text: () => Promise; 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/graphql.ts: -------------------------------------------------------------------------------- 1 | export type GraphqlOperationType = 'Query' | 'Mutation'; 2 | 3 | /** 4 | * A Graphql Request 5 | */ 6 | export interface GraphqlRequest { 7 | /** 8 | * The full request query 9 | */ 10 | query: string; 11 | 12 | /** 13 | * The parsed operation name 14 | */ 15 | operationName?: string; 16 | 17 | /** 18 | * The operation type 19 | */ 20 | operationType: GraphqlOperationType; 21 | 22 | /** 23 | * The query variables 24 | */ 25 | variables?: Record; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts'; 2 | export * from './event'; 3 | export * from './fetch'; 4 | export * from './graphql'; 5 | export * from './http'; 6 | export * from './json'; 7 | export * from './middleware'; 8 | export * from './nanoid'; 9 | export * from './options'; 10 | export * from './payload'; 11 | export * from './plugin'; 12 | export * from './sanity'; 13 | export * from './time'; 14 | export * from './websocket'; 15 | -------------------------------------------------------------------------------- /packages/core/src/json.test.ts: -------------------------------------------------------------------------------- 1 | import { safeParseJson } from './json'; 2 | 3 | describe('safeParseJson', () => { 4 | it('should return parsed JSON if input is valid', () => { 5 | const result = safeParseJson('{"foo":"bar"}'); 6 | expect(result).toEqual({ value: { foo: 'bar' } }); 7 | }); 8 | 9 | it('should return null if input is invalid', () => { 10 | const result = safeParseJson('{"foo"'); 11 | expect(result).toEqual({ error: new SyntaxError('Unexpected end of JSON input') }); 12 | }); 13 | 14 | it('should return null if input is undefined', () => { 15 | const result = safeParseJson(undefined); 16 | expect(result).toEqual({}); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/src/json.ts: -------------------------------------------------------------------------------- 1 | type SafeParseJsonResult = { 2 | value?: T; 3 | error?: any; 4 | }; 5 | 6 | export function safeParseJson(data: string | null | undefined): SafeParseJsonResult { 7 | if (!data) return {}; 8 | try { 9 | const value = JSON.parse(data) as T; 10 | return { value }; 11 | } catch (error) { 12 | return { error }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/log.ts: -------------------------------------------------------------------------------- 1 | // TODO: would be good to find a way to have a universal logger 2 | // for envy, perhaps via window|globals 3 | export interface Log { 4 | info: (msg: string, ...args: unknown[]) => void; 5 | warn: (msg: string, ...args: unknown[]) => void; 6 | error: (msg: string, ...args: unknown[]) => void; 7 | debug: (msg: string, ...args: unknown[]) => void; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/middleware/graphql.ts: -------------------------------------------------------------------------------- 1 | import { GraphqlRequest, safeParseJson } from '..'; 2 | 3 | import { Middleware } from '.'; 4 | 5 | export const Graphql: Middleware = event => { 6 | if (event.http) { 7 | const httpRequest = event.http; 8 | 9 | if (httpRequest.method === 'GET') { 10 | const url = new URL(httpRequest.url); 11 | 12 | event.graphql = mapGraphqlData({ 13 | operationName: url.searchParams.get('operationName'), 14 | query: url.searchParams.get('query'), 15 | variables: safeParseJson(url.searchParams.get('variables')).value, 16 | }); 17 | } 18 | 19 | if ( 20 | httpRequest.method === 'POST' && 21 | httpRequest.requestBody && 22 | httpRequest.requestHeaders['content-type'] === 'application/json' 23 | ) { 24 | const json = safeParseJson(httpRequest.requestBody); 25 | if (json.value) { 26 | event.graphql = mapGraphqlData(json.value); 27 | } 28 | } 29 | } 30 | return event; 31 | }; 32 | 33 | function mapGraphqlData({ 34 | operationName, 35 | query, 36 | variables, 37 | }: { 38 | operationName?: string | null; 39 | query?: string | null; 40 | variables?: Record; 41 | }): GraphqlRequest | undefined { 42 | const matcher = /^(mutation|query)?(\s*)?([a-zA-Z]*)?(\s*)?(\(.*\))?(\s*)?{/g; 43 | const match = query && matcher.exec(query.trim()); 44 | if (match) { 45 | return { 46 | operationType: (match[1] || 'query') as GraphqlRequest['operationType'], 47 | operationName: operationName || undefined, 48 | query, 49 | variables: variables || undefined, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../event'; 2 | import { Options } from '../options'; 3 | 4 | export type Middleware = (event: Event, options: Options) => Event; 5 | 6 | export { Sanity } from './sanity'; 7 | export { Meta } from './meta'; 8 | export { Graphql } from './graphql'; 9 | -------------------------------------------------------------------------------- /packages/core/src/middleware/meta.test.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '../event'; 2 | 3 | import { Meta } from './meta'; 4 | 5 | describe('meta', () => { 6 | it('should add the service name', () => { 7 | const event = {} as Event; 8 | const output = Meta(event, { serviceName: 'test-name' }); 9 | expect(output.serviceName).toBe('test-name'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/core/src/middleware/meta.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '.'; 2 | 3 | export const Meta: Middleware = (event, options) => { 4 | event.serviceName = options.serviceName; 5 | return event; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/core/src/middleware/sanity.ts: -------------------------------------------------------------------------------- 1 | import { safeParseJson } from '../json'; 2 | 3 | import { Middleware } from '.'; 4 | 5 | const HOST = '.sanity.io'; 6 | 7 | export const Sanity: Middleware = event => { 8 | if (event.http) { 9 | const httpRequest = event.http; 10 | 11 | if (httpRequest.host.endsWith(HOST)) { 12 | let query: string | null = null; 13 | switch (httpRequest.method) { 14 | case 'GET': { 15 | const [, qs] = (httpRequest.path ?? '').split('?'); 16 | if (qs) { 17 | query = decodeURIComponent(qs).replace('query=', ''); 18 | } 19 | break; 20 | } 21 | case 'POST': { 22 | if (httpRequest.requestBody && httpRequest.requestHeaders['content-type'] === 'application/json') { 23 | const json = safeParseJson(httpRequest.requestBody); 24 | query = json.value?.query; 25 | } 26 | 27 | break; 28 | } 29 | } 30 | const queryType = query ? /_type\s*==\s*['"](.*?)['"]/m.exec(query)?.[1] ?? null : null; 31 | 32 | event.sanity = { 33 | query: query || undefined, 34 | queryType: queryType || undefined, 35 | }; 36 | } 37 | } 38 | return event; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/nanoid.ts: -------------------------------------------------------------------------------- 1 | export type CryptoModule = { 2 | getRandomValues(array: T): T; 3 | }; 4 | 5 | // cross compatible nanoid implementation from 6 | // https://github.com/ai/nanoid 7 | export const nanoid = 8 | (crypto: CryptoModule) => 9 | (t = 21) => 10 | crypto 11 | .getRandomValues(new Uint8Array(t)) 12 | .reduce( 13 | (t: any, e: any) => 14 | (t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e < 63 ? '_' : '-'), 15 | '', 16 | ); 17 | -------------------------------------------------------------------------------- /packages/core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '.'; 2 | 3 | export type FilterableHttpRequest = Pick; 4 | 5 | export interface Options { 6 | /** 7 | * A unique identifier for the application 8 | */ 9 | serviceName: string; 10 | 11 | /** 12 | * Set to true to enable debugging of exported messages 13 | * @default false 14 | */ 15 | debug?: boolean; 16 | 17 | /** 18 | * Define a function to filter http requests 19 | * @default undefined 20 | */ 21 | filter?: (request: FilterableHttpRequest) => boolean; 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/payload.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './event'; 2 | 3 | // how the 'connections' data will arrive from the collector 4 | // [serviceName: string, isActive: boolean][] 5 | export type ConnectionStatusData = [string, boolean][]; 6 | 7 | export type ConnectionStatusPayload = { 8 | type: 'connections'; 9 | data: ConnectionStatusData; 10 | }; 11 | 12 | export type TracePayload = { 13 | type: 'trace'; 14 | data: Event; 15 | }; 16 | 17 | export type WebSocketPayload = ConnectionStatusPayload | TracePayload; 18 | -------------------------------------------------------------------------------- /packages/core/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './event'; 2 | import { Options } from './options'; 3 | 4 | export interface Exporter { 5 | send: (data: Event) => void; 6 | } 7 | 8 | export type Plugin = (options: Options, exporter: Exporter) => void; 9 | -------------------------------------------------------------------------------- /packages/core/src/sanity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Sanity Request 3 | */ 4 | export interface SanityRequest { 5 | /** 6 | * The full request query 7 | */ 8 | query?: string | null; 9 | 10 | /** 11 | * The sanity type used in the query 12 | */ 13 | queryType?: string | null; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/time.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_RETRY_DELAY = 250; 2 | const DEFAULT_RETRY_FACTOR = 2; // how sharply the retry delay rises 3 | const DEFAULT_RETRY_FLATTEN = 6; // how slowly the retry delay rises 4 | const DEFAULT_RETRY_MAX_ATTEMPTS = 30; 5 | 6 | /** 7 | * Tracks expoonential time delay for retries 8 | */ 9 | export class Retry { 10 | private retryDelay = DEFAULT_RETRY_DELAY; 11 | private retryAttempts = 0; 12 | 13 | /** 14 | * The number of attempts 15 | */ 16 | get attempts() { 17 | return this.retryAttempts; 18 | } 19 | 20 | /** 21 | * The current delay in ms 22 | */ 23 | get delay() { 24 | return this.retryDelay; 25 | } 26 | 27 | /** 28 | * Returns true if the max attempts has not been exceeded 29 | */ 30 | get shouldRetry() { 31 | return this.retryAttempts >= DEFAULT_RETRY_MAX_ATTEMPTS; 32 | } 33 | 34 | /** 35 | * Increments the attempts and returns a new delay in ms 36 | * @returns the delay in ms 37 | */ 38 | getNextDelay() { 39 | const jitter = Math.random() * 1000; 40 | const exp = Math.pow(this.retryAttempts++ / DEFAULT_RETRY_FLATTEN, DEFAULT_RETRY_FACTOR) * 1000; 41 | this.retryDelay = jitter + exp; 42 | return this.retryDelay; 43 | } 44 | 45 | /** 46 | * Reset the attempts and delay to defaults 47 | */ 48 | reset() { 49 | this.retryDelay = DEFAULT_RETRY_DELAY; 50 | this.retryAttempts = 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/url.test.ts: -------------------------------------------------------------------------------- 1 | import { tryParseURL } from './url'; 2 | 3 | describe('url', () => { 4 | const cases = [ 5 | ['/relative', undefined, undefined], 6 | ['/relative', 'localhost:3001', undefined], 7 | ['/relative', 'http://localhost:3001', 'http://localhost:3001/relative'], 8 | ['http://localhost:3001/path', undefined, 'http://localhost:3001/path'], 9 | [new URL('http://localhost:3001/another'), undefined, 'http://localhost:3001/another'], 10 | ]; 11 | 12 | it.each(cases)('should tryparse', (url: any, base: any, expected: any) => { 13 | expect(tryParseURL(url, base)?.href).toEqual(expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/src/url.ts: -------------------------------------------------------------------------------- 1 | export function tryParseURL(url: string | URL, base?: string): URL | undefined { 2 | // implement cross platform `URL.canParse` since this function 3 | // only exists in the DOM and not in Node 4 | try { 5 | return new URL(url, base); 6 | } catch {} 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["**/*.test.ts"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/nextjs/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '@envyjs/core'; 2 | import { NextjsTracingOptions } from '@envyjs/nextjs'; 3 | 4 | declare global { 5 | interface Window { 6 | envy: Options; 7 | } 8 | 9 | // eslint-disable-next-line no-var 10 | var envy: NextjsTracingOptions; 11 | } 12 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/nextjs", 3 | "version": "0.10.1", 4 | "description": "Node.js Network & Telemetry Viewer", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "homepage": "https://github.com/formidablelabs/envy", 11 | "keywords": [ 12 | "react", 13 | "nextjs", 14 | "graphql", 15 | "typescript", 16 | "nodejs", 17 | "telemetry", 18 | "tracing" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/FormidableLabs/envy.git", 23 | "directory": "packages/nextjs" 24 | }, 25 | "license": "MIT", 26 | "publishConfig": { 27 | "provenance": true 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "prebuild": "rimraf dist", 35 | "build": "tsc" 36 | }, 37 | "dependencies": { 38 | "@envyjs/node": "0.10.1", 39 | "@envyjs/web": "0.10.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/nextjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tracing'; 2 | export * from './webpack'; 3 | -------------------------------------------------------------------------------- /packages/nextjs/src/loaders/config.loader.ts: -------------------------------------------------------------------------------- 1 | export default function envyWebpackLoader(this: any, source: string) { 2 | const options = this.getOptions(); 3 | 4 | let injectedCode = 'var _envGlobalObject = typeof window === "undefined" ? global : window;\n'; 5 | injectedCode += `_envGlobalObject.envy=${JSON.stringify(options)};\n`; 6 | return `${injectedCode}\n${source}`; 7 | } 8 | -------------------------------------------------------------------------------- /packages/nextjs/src/loaders/page.loader.ts: -------------------------------------------------------------------------------- 1 | export default function envyPageLoader(this: any, source: string) { 2 | const options = this.getOptions(); 3 | const injectedCode = ` 4 | const { enableTracing } = require('@envyjs/nextjs'); 5 | enableTracing(${JSON.stringify(options)}); 6 | `; 7 | return `${injectedCode}\n${source}`; 8 | } 9 | -------------------------------------------------------------------------------- /packages/nextjs/src/loaders/server.loader.ts: -------------------------------------------------------------------------------- 1 | import { enableTracing } from '../tracing'; 2 | 3 | // global.envy is injected by webpack 4 | const options = global.envy; 5 | enableTracing(options); 6 | -------------------------------------------------------------------------------- /packages/nextjs/src/loaders/web.loader.ts: -------------------------------------------------------------------------------- 1 | import { enableTracing } from '@envyjs/web'; 2 | 3 | // window.envy is injected by webpack 4 | const options = window.envy; 5 | enableTracing(options); 6 | -------------------------------------------------------------------------------- /packages/nextjs/src/route.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@envyjs/core'; 2 | 3 | export const Routes: Plugin = () => { 4 | // intentionally blank 5 | }; 6 | -------------------------------------------------------------------------------- /packages/nextjs/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_WEB_SOCKET_PORT } from '@envyjs/core'; 2 | import { TracingOptions, enableTracing as nodeTracing } from '@envyjs/node'; 3 | 4 | import { Routes } from './route'; 5 | 6 | type GlobalWithFlag = { __nextjsTracingInitialized: boolean }; 7 | const globalWithFlag: GlobalWithFlag = global as unknown as GlobalWithFlag; 8 | 9 | // eslint-disable-next-line @typescript-eslint/ban-types 10 | export type NextjsTracingOptions = TracingOptions & {}; 11 | 12 | export function enableTracing(options: NextjsTracingOptions) { 13 | const nextjsOptions: NextjsTracingOptions = { 14 | ...options, 15 | }; 16 | 17 | // Exclude the following traces 18 | // 127.0.0.1:9999 websocket upgrade protocol 19 | // (ip address):\d{5} nextjs websocket 20 | const matcher = new RegExp(`(127\\.0\\.0\\.1|localhost|\\[::1\\]):(${DEFAULT_WEB_SOCKET_PORT}|\\d{5})`); 21 | 22 | nextjsOptions.filter = request => { 23 | if (matcher.test(request.url)) return false; 24 | 25 | if (options.filter) { 26 | return options.filter(request); 27 | } 28 | 29 | return true; 30 | }; 31 | 32 | if (!globalWithFlag.__nextjsTracingInitialized) { 33 | globalWithFlag.__nextjsTracingInitialized = true; 34 | return nodeTracing({ 35 | ...nextjsOptions, 36 | plugins: [...(options.plugins || []), Routes], 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["globals.d.ts", "src/**/*"], 4 | "exclude": ["**/*.spec.ts"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/node/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/node", 3 | "version": "0.10.1", 4 | "description": "Node.js Network & Telemetry Viewer", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "homepage": "https://github.com/formidablelabs/envy", 11 | "keywords": [ 12 | "react", 13 | "nextjs", 14 | "graphql", 15 | "typescript", 16 | "nodejs", 17 | "telemetry", 18 | "tracing" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/FormidableLabs/envy.git", 23 | "directory": "packages/node" 24 | }, 25 | "license": "MIT", 26 | "publishConfig": { 27 | "provenance": true 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "prebuild": "rimraf dist", 35 | "build": "tsc", 36 | "test:live": "ts-node ./src/test.ts" 37 | }, 38 | "dependencies": { 39 | "@envyjs/core": "0.10.1", 40 | "chalk": "^4.1.2", 41 | "shimmer": "^1.2.1", 42 | "ws": "8.14.2" 43 | }, 44 | "devDependencies": { 45 | "@types/node-fetch": "^2.6.4", 46 | "@types/shimmer": "^1.0.2", 47 | "node-fetch": "^2.7.0", 48 | "ts-node": "^10.9.1" 49 | }, 50 | "optionalDependencies": { 51 | "bufferutil": "4.0.7", 52 | "utf-8-validate": "6.0.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/node/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Plugin, 3 | getEventFromAbortedFetchRequest, 4 | getEventFromFetchRequest, 5 | getEventFromFetchResponse, 6 | } from '@envyjs/core'; 7 | 8 | import { generateId } from './id'; 9 | 10 | export const Fetch: Plugin = (_options, exporter) => { 11 | const { fetch: originalFetch } = global; 12 | global.fetch = async (...args) => { 13 | const id = generateId(); 14 | const startTs = performance.now(); 15 | let response: Response | undefined = undefined; 16 | 17 | // export the initial request data 18 | const reqEvent = getEventFromFetchRequest(id, ...args); 19 | exporter.send(reqEvent); 20 | 21 | // if the args contain a signal, listen for abort events 22 | const signal = args[1]?.signal; 23 | if (signal) { 24 | signal.addEventListener('abort', () => { 25 | // if the request has already been resolved, we don't need to do anything 26 | if (response) return; 27 | 28 | // export the aborted request data 29 | const duration = performance.now() - startTs; 30 | const abortEvent = getEventFromAbortedFetchRequest(reqEvent, duration); 31 | exporter.send(abortEvent); 32 | }); 33 | } 34 | 35 | // execute the actual request 36 | response = await originalFetch(...args); 37 | const responseClone = response.clone(); 38 | const resEvent = await getEventFromFetchResponse(reqEvent, responseClone); 39 | 40 | resEvent.http!.duration = performance.now() - startTs; 41 | 42 | // export the final request data which now includes response 43 | exporter.send(resEvent); 44 | 45 | return response; 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/node/src/id.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'node:crypto'; 2 | 3 | import { nanoid } from '@envyjs/core'; 4 | 5 | export const generateId = nanoid(webcrypto as any); 6 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tracing'; 2 | -------------------------------------------------------------------------------- /packages/node/src/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk'; 3 | 4 | const { name } = require('../package.json'); 5 | 6 | export default { 7 | info: (msg: string, ...args: unknown[]) => console.log(chalk.green(`✅ ${name}`, msg), ...args), 8 | warn: (msg: string, ...args: unknown[]) => console.log(chalk.yellow(`🚸 ${name}`, msg), ...args), 9 | error: (msg: string, ...args: unknown[]) => console.log(chalk.red(`❌ ${name}`, msg), ...args), 10 | debug: (msg: string, ...args: unknown[]) => console.log(chalk.cyan(`🔧 ${name}`, msg), ...args), 11 | }; 12 | -------------------------------------------------------------------------------- /packages/node/src/test.ts: -------------------------------------------------------------------------------- 1 | // HACK: convert to jest test that captures the output 2 | // from console and verifies it against expected values 3 | 4 | /* eslint-disable import/order */ 5 | import log from './log'; 6 | import { enableTracing } from './tracing'; 7 | 8 | // must happen first in order to wrap http/https 9 | enableTracing({ 10 | debug: true, 11 | serviceName: 'unicorns', 12 | }); 13 | 14 | import fetch from 'node-fetch'; 15 | 16 | // test against node-fetch 17 | fetch('https://api.quotable.io/quotes/random', { 18 | headers: { 19 | Authorization: 'Bearer 12345', 20 | }, 21 | }) 22 | .then(response => response.json()) 23 | .then((quote: Array<{ content: string }>) => { 24 | log.info('Quote Fetched:', quote[0].content); 25 | }); 26 | 27 | // gzip test 28 | fetch('https://xkcd.com/info.0.json') 29 | .then(response => response.json()) 30 | .then(item => { 31 | log.info('xkcd', { 32 | id: Buffer.from(item.img).toString('base64'), 33 | title: item.title, 34 | imageUrl: item.img, 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/node/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { Exporter, Graphql, Meta, Middleware, Options, Plugin, Sanity, WebSocketClient } from '@envyjs/core'; 2 | 3 | import { Fetch } from './fetch'; 4 | import { Http } from './http'; 5 | import log from './log'; 6 | 7 | export interface TracingOptions extends Options { 8 | plugins?: Plugin[]; 9 | port?: number; 10 | } 11 | 12 | export function enableTracing(options: TracingOptions) { 13 | if (options.debug) { 14 | log.info('debug mode'); 15 | } 16 | 17 | // custom websocket client 18 | const wsClient = WebSocketClient({ 19 | ...options, 20 | clientName: 'node', 21 | log, 22 | }); 23 | 24 | // middleware transforms event data 25 | const middleware: Middleware[] = [Meta, Sanity, Graphql]; 26 | 27 | // apply the middleware and send with the websocket 28 | const exporter: Exporter = { 29 | send(message) { 30 | const result = middleware.reduce((prev, t) => t(prev, options), message); 31 | if (result) { 32 | if (result.http && options.filter && options.filter(result.http) === false) { 33 | return; 34 | } 35 | wsClient.send(result); 36 | } 37 | }, 38 | }; 39 | 40 | // initialize all plugins 41 | [Http, Fetch, ...(options.plugins || [])].forEach(fn => fn(options, exporter)); 42 | } 43 | -------------------------------------------------------------------------------- /packages/node/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | // HAR timing Data adapted from 2 | // https://github.com/exogen/node-fetch-har/blob/master/index.js 3 | 4 | import { HttpRequest } from '@envyjs/core'; 5 | 6 | export type HRTime = [number, number]; 7 | 8 | export type Timestamps = { 9 | firstByte?: HRTime; 10 | start: HRTime; 11 | socket?: HRTime; 12 | lookup?: HRTime; 13 | connect?: HRTime; 14 | received?: HRTime; 15 | secureConnect?: HRTime; 16 | sent?: HRTime; 17 | }; 18 | 19 | export function calculateTiming(time: Timestamps): HttpRequest['timings'] { 20 | // For backwards compatibility with HAR 1.1, the `connect` timing 21 | // includes `ssl` instead of being mutually exclusive. 22 | const legacyConnnect = time.secureConnect || time.connect; 23 | 24 | const blocked = getDuration(time.start, time.socket); 25 | const dns = getDuration(time.socket, time.lookup); 26 | const connect = getDuration(time.lookup, legacyConnnect); 27 | 28 | let ssl = -1; 29 | if (time.secureConnect) { 30 | ssl = getDuration(time.connect, time.secureConnect); 31 | } 32 | 33 | const send = getDuration(legacyConnnect, time.sent); 34 | const wait = Math.max(getDuration(time.sent, time.firstByte), 0); 35 | const receive = getDuration(time.firstByte, time.received); 36 | 37 | return { 38 | blocked, 39 | dns, 40 | connect, 41 | send, 42 | wait, 43 | receive, 44 | ssl, 45 | }; 46 | } 47 | 48 | export function getDuration(a: HRTime | undefined, b: HRTime | undefined): number { 49 | if (!(a && b)) return 0; 50 | const seconds = b[0] - a[0]; 51 | const nanoseconds = b[1] - a[1]; 52 | return seconds * 1000 + nanoseconds / 1e6; 53 | } 54 | -------------------------------------------------------------------------------- /packages/node/src/utils/wrap.ts: -------------------------------------------------------------------------------- 1 | import { types as utilTypes } from 'util'; 2 | 3 | import { wrap as _wrap } from 'shimmer'; 4 | 5 | // ESM handling of wrapping 6 | export const wrap: typeof _wrap = (moduleExports, name, wrapper) => { 7 | if (!utilTypes.isProxy(moduleExports)) { 8 | return _wrap(moduleExports, name, wrapper); 9 | } else { 10 | const wrapped = _wrap(Object.assign({}, moduleExports), name, wrapper); 11 | 12 | return Object.defineProperty(moduleExports, name, { 13 | value: wrapped, 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["**/*.spec.ts"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@envyjs/web", 3 | "version": "0.10.1", 4 | "description": "Node.js Network & Telemetry Viewer", 5 | "main": "dist/index.js", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "homepage": "https://github.com/formidablelabs/envy", 11 | "keywords": [ 12 | "react", 13 | "nextjs", 14 | "graphql", 15 | "typescript", 16 | "nodejs", 17 | "telemetry", 18 | "tracing" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/FormidableLabs/envy.git", 23 | "directory": "packages/web" 24 | }, 25 | "license": "MIT", 26 | "publishConfig": { 27 | "provenance": true 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "prebuild": "rimraf dist", 35 | "build": "tsc" 36 | }, 37 | "dependencies": { 38 | "@envyjs/core": "0.10.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/web/src/id.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@envyjs/core'; 2 | 3 | export const generateId = nanoid(crypto); 4 | -------------------------------------------------------------------------------- /packages/web/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tracing'; 2 | -------------------------------------------------------------------------------- /packages/web/src/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const { name } = require('../package.json'); 4 | 5 | export default { 6 | info: (msg: string, ...args: unknown[]) => console.log(`✅ %c${name} ${msg}`, 'color: green', ...args), 7 | warn: (msg: string, ...args: unknown[]) => console.log(`🚸 %c${name} ${msg}`, 'color: yellow', ...args), 8 | error: (msg: string, ...args: unknown[]) => console.log(`❌ %c${name} ${msg}`, 'color: red', ...args), 9 | debug: (msg: string, ...args: unknown[]) => console.log(`🔧 %c$${name} ${msg}`, 'color: cyan', ...args), 10 | }; 11 | -------------------------------------------------------------------------------- /packages/web/src/performance.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest } from '@envyjs/core'; 2 | 3 | /** 4 | * Calculate timing data from performance API data 5 | * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming 6 | */ 7 | export function calculateTiming(time: PerformanceResourceTiming | undefined): { 8 | duration?: number; 9 | timings?: HttpRequest['timings']; 10 | } { 11 | // Some data is not available if the resource is a cross-origin request 12 | // and no Timing-Allow-Origin HTTP response header is used 13 | // 14 | // Check if we have this data available, and if we do, we can 15 | // improve the timing data with it 16 | if (!time?.requestStart) return {}; 17 | 18 | const timings: HttpRequest['timings'] = { 19 | blocked: time.fetchStart - time.startTime, 20 | dns: time.domainLookupEnd - time.domainLookupStart, 21 | connect: time.connectEnd - time.connectStart, 22 | send: 0, // there is no data available for this in native fetch 23 | wait: time.responseStart - time.requestStart, 24 | receive: time.responseEnd - time.responseStart, 25 | ssl: -1, 26 | }; 27 | 28 | if (time.secureConnectionStart) { 29 | timings.ssl = time.connectEnd - time.secureConnectionStart; 30 | } 31 | 32 | return { 33 | duration: time.duration, 34 | timings, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/web/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { Exporter, Graphql, Meta, Middleware, Options, Plugin, Sanity, WebSocketClient } from '@envyjs/core'; 2 | 3 | import { Fetch } from './fetch'; 4 | import log from './log'; 5 | 6 | export interface TracingOptions extends Options { 7 | plugins?: Plugin[]; 8 | port?: number; 9 | } 10 | 11 | export function enableTracing(options: TracingOptions): void { 12 | if (typeof window === 'undefined') { 13 | log.error('Attempted to use @envyjs/web in a non-browser environment'); 14 | } 15 | 16 | if (options.debug) log.info('Starting in debug mode'); 17 | 18 | // custom websocket client 19 | const ws = WebSocketClient({ 20 | ...options, 21 | clientName: 'web', 22 | log, 23 | }); 24 | 25 | // middleware transforms event data 26 | const middleware: Middleware[] = [Meta, Sanity, Graphql]; 27 | 28 | // apply the middleware and send with the websocket 29 | const exporter: Exporter = { 30 | send(message) { 31 | const result = middleware.reduce((prev, t) => t(prev, options), message); 32 | if (result.http && options.filter && options.filter(result.http) === false) { 33 | return; 34 | } 35 | ws.send(result); 36 | }, 37 | }; 38 | 39 | // initialize all plugins 40 | [Fetch, ...(options.plugins || [])].forEach(fn => fn(options, exporter)); 41 | } 42 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["**/*.spec.ts"], 5 | "compilerOptions": { 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "outDir": "dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/webui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootConfig = require('../../.eslintrc.cjs'); 4 | 5 | module.exports = { 6 | ...rootConfig, 7 | extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended', ...rootConfig.extends], 8 | settings: { 9 | 'react': { version: 'detect' }, 10 | 'import/parsers': { 11 | '@typescript-eslint/parser': ['.ts', '.tsx'], 12 | }, 13 | 'import/resolver': { 14 | typescript: { 15 | project: 'packages/webui/tsconfig.json', 16 | }, 17 | alias: [['@', path.resolve(__dirname, './src')]], 18 | }, 19 | }, 20 | rules: { 21 | 'react/button-has-type': 'off', 22 | 'react-hooks/exhaustive-deps': 'error', 23 | 'react/jsx-key': ['error', { checkFragmentShorthand: true, warnOnDuplicates: true }], 24 | 'react/prop-types': 'off', 25 | 'react/react-in-jsx-scope': 'off', 26 | ...rootConfig.rules, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/webui/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "resolvers": ["./pkg/demoResolver.js", "..."] 4 | } 5 | -------------------------------------------------------------------------------- /packages/webui/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/webui/.storybook/addons/preset.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | const MOCKS_DIRECTORY = "__storybook__mocks__"; 5 | 6 | // based on https://github.com/gebeto/storybook-addon-manual-mocks 7 | // modified to work with a different directory to avoid 8 | // conflicts with jest mocks 9 | export async function viteFinal(config) { 10 | const { mergeConfig } = await import("vite"); 11 | function parcelMocksPlugin() { 12 | return { 13 | name: "mocks-plugin", 14 | load(_importPath) { 15 | const importPath = _importPath.replace(/\0/g, ""); 16 | const basePath = path.parse(importPath); 17 | const mockPath = path.join( 18 | basePath.dir, 19 | MOCKS_DIRECTORY, 20 | basePath.base 21 | ); 22 | const isReplacementPathExists = fs.existsSync(mockPath); 23 | if (isReplacementPathExists) { 24 | return fs.readFileSync(mockPath, { encoding: "utf8" }); 25 | } 26 | }, 27 | }; 28 | } 29 | 30 | return mergeConfig(config, { 31 | plugins: [parcelMocksPlugin()], 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/webui/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | import { viteFinal } from './addons/preset'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-styling', 9 | ], 10 | framework: { 11 | name: '@storybook/react-vite', 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: 'tag', 16 | }, 17 | viteFinal 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /packages/webui/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../src/styles/base.css'; 2 | 3 | import { withThemeByClassName } from '@storybook/addon-styling'; 4 | 5 | export const decorators = [ 6 | withThemeByClassName({ 7 | themes: { 8 | light: 'light', 9 | dark: 'dark', 10 | }, 11 | defaultTheme: 'light', 12 | }), 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/webui/README.md: -------------------------------------------------------------------------------- 1 | # Envy 2 | 3 | This package is part of the Envy Developer Toolset. Please refer to the main [README](https://github.com/FormidableLabs/envy#readme) for usage. 4 | -------------------------------------------------------------------------------- /packages/webui/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/src/**/?(*.)+(spec|test).[jt]s?(x)'], 3 | testEnvironment: 'jsdom', 4 | setupFiles: ['/src/testing/setupJest.ts'], 5 | setupFilesAfterEnv: ['/src/testing/setupJestAfterEnv.ts'], 6 | globalSetup: '/src/testing/setupJestGlobal.ts', 7 | moduleNameMapper: { 8 | '^@/(.*)$': '/src/$1', 9 | 10 | // support css imports in react components 11 | '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy', 12 | }, 13 | collectCoverageFrom: [ 14 | './src/**/*.{ts,tsx}', 15 | '!./src/testing/**/*.{ts,tsx}', 16 | '!./src/**/*.stories.{ts,tsx}', 17 | '!./src/**/__storybook__mocks__/*.ts', 18 | '!**/*.d.ts', 19 | '!**/node_modules/**', 20 | '!**/dist/**', 21 | '!**/bin/**', 22 | '!**/coverage/**', 23 | ], 24 | transform: { 25 | // default ts-jest preset 26 | '^.+\\.tsx?$': 'ts-jest', 27 | 28 | // support for mjs files 29 | '^.+\\.mjs?$': [ 30 | 'babel-jest', 31 | { 32 | presets: ['@babel/preset-env'], 33 | targets: { 34 | node: 'current', 35 | }, 36 | }, 37 | ], 38 | }, 39 | transformIgnorePatterns: ['/node_modules/(?!allotment).+\\.mjs$'], 40 | }; 41 | -------------------------------------------------------------------------------- /packages/webui/pkg/demoResolver.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { Resolver } from '@parcel/plugin'; 4 | 5 | const isProduction = process.env.NODE_ENV === 'production'; 6 | const isDemo = process.env.DEMO === 'true'; 7 | 8 | const productionCode = ` 9 | const mockTraces = []; 10 | export default mockTraces; 11 | export function generateLotsOfMockTraces() { 12 | return []; 13 | } 14 | export function mockTraceCollection() { 15 | return new Map(); 16 | } 17 | `; 18 | 19 | export default new Resolver({ 20 | async resolve({ specifier }) { 21 | // remove mock traces in production unless its a demo 22 | if (isProduction && !isDemo) { 23 | if (specifier === '@/testing/mockTraces') { 24 | return { 25 | filePath: path.resolve('dummy.ts'), 26 | code: productionCode, 27 | }; 28 | } 29 | } 30 | return null; 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/webui/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | 3 | import App from './App'; 4 | 5 | describe('App', () => { 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | 10 | it('should render without error', () => { 11 | render(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/ui/Header'; 2 | import MainDisplay from '@/components/ui/MainDisplay'; 3 | import ApplicationContextProvider from '@/context/ApplicationContext'; 4 | 5 | export default function App() { 6 | return ( 7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/webui/src/collector/__storybook__mocks__/CollectorClient.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStatusData, DEFAULT_WEB_SOCKET_PORT, Event } from '@envyjs/core'; 2 | 3 | import { mockTraceCollection } from '@/testing/mockTraces'; 4 | import { Traces } from '@/types'; 5 | 6 | export const MockCollectorData: { 7 | connected: boolean; 8 | connecting: boolean; 9 | connections: ConnectionStatusData; 10 | traces: Traces; 11 | } = { 12 | connected: true, 13 | connecting: false, 14 | connections: [ 15 | ['node-client', true], 16 | ['web-client', true], 17 | ['ts-client', false], 18 | ], 19 | traces: mockTraceCollection(), 20 | }; 21 | 22 | type WebSocketClientOptions = { 23 | port?: number; 24 | changeHandler?: () => void; 25 | }; 26 | 27 | export default class CollectorClient { 28 | private readonly _port: number; 29 | private _changeHandler?: (newTraceId?: string) => void; 30 | 31 | constructor({ port, changeHandler }: WebSocketClientOptions) { 32 | this._port = port ?? DEFAULT_WEB_SOCKET_PORT; 33 | this._changeHandler = changeHandler; 34 | } 35 | 36 | get port() { 37 | return '8080'; 38 | } 39 | 40 | get traces() { 41 | return MockCollectorData.traces; 42 | } 43 | 44 | get connected() { 45 | return MockCollectorData.connected; 46 | } 47 | 48 | get connecting() { 49 | return MockCollectorData.connecting; 50 | } 51 | 52 | get connections() { 53 | return MockCollectorData.connections; 54 | } 55 | 56 | private _signalChange(newTraceId?: string) { 57 | this._changeHandler?.(newTraceId); 58 | } 59 | 60 | start() { 61 | // intentionally left blank 62 | } 63 | 64 | addEvent(event: Event) { 65 | const trace = { ...event }; 66 | const isNewTrace = !MockCollectorData.traces.has(trace.id); 67 | MockCollectorData.traces.set(trace.id, trace); 68 | this._signalChange(isNewTrace ? trace.id : undefined); 69 | } 70 | 71 | clearTraces() { 72 | MockCollectorData.traces.clear(); 73 | this._signalChange(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/webui/src/components/Authorization.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Authorization from './Authorization'; 4 | 5 | const meta = { 6 | title: 'Components/Authorization', 7 | component: Authorization, 8 | parameters: { 9 | layout: 'centered', 10 | }, 11 | decorators: [ 12 | Story => ( 13 |
14 | 15 |
16 | ), 17 | ], 18 | } satisfies Meta; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Standard: Story = { 24 | args: { 25 | value: 26 | 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.vqb33-7FqzFWPNlr0ElW1v2RjJRZBel3CdDHBWD7y_o', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/webui/src/components/Badge.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Badge from './Badge'; 4 | 5 | const meta = { 6 | title: 'Components/Badge', 7 | component: Badge, 8 | parameters: { 9 | layout: 'centered', 10 | }, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Standard: Story = { 17 | args: { 18 | children: 'Label', 19 | className: 'bg-gray-100', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/webui/src/components/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | 3 | import Badge from './Badge'; 4 | 5 | describe('badge', () => { 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | 10 | it('should not overwrite base css classes', () => { 11 | const { container } = render(Label); 12 | expect(container.firstChild).toHaveClass('lkj'); 13 | expect(container.firstChild).toHaveClass('inline-flex'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/webui/src/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | 3 | import { tw } from '@/utils'; 4 | 5 | type BadgeProps = HTMLAttributes; 6 | 7 | export default function Badge({ className, children }: BadgeProps) { 8 | return ( 9 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/webui/src/components/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Trash } from 'lucide-react'; 3 | 4 | import Button from './Button'; 5 | 6 | const meta = { 7 | title: 'Components/Button', 8 | component: Button, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | argTypes: { 13 | selected: { 14 | control: { type: 'boolean' }, 15 | }, 16 | size: { 17 | options: ['small', 'standard'], 18 | control: { type: 'select' }, 19 | }, 20 | border: { 21 | options: ['standard', 'none'], 22 | control: { type: 'select' }, 23 | }, 24 | }, 25 | } satisfies Meta; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const TextOnly: Story = { 31 | args: { 32 | children: 'Standard Button', 33 | size: 'standard', 34 | border: 'standard', 35 | }, 36 | }; 37 | 38 | export const IconOnly: Story = { 39 | args: { 40 | Icon: Trash, 41 | }, 42 | }; 43 | 44 | export const IconButton: Story = { 45 | args: { 46 | children: 'Trash Can', 47 | Icon: Trash, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/webui/src/components/Code.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import Code from './Code'; 4 | 5 | const meta = { 6 | title: 'Components/Code', 7 | component: Code, 8 | parameters: { 9 | layout: 'centered', 10 | }, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Standard: Story = { 17 | args: { 18 | children: `import React from 'react'; 19 | import { render } from 'react-dom'; 20 | 21 | function SomeComponent({ children }: { children: React.ReactNode }) { 22 | return
Some Component
; 23 | }`, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/webui/src/components/Code.tsx: -------------------------------------------------------------------------------- 1 | import { prettyFormat, tw } from '@/utils'; 2 | 3 | type CodeProps = Omit, 'children'> & { 4 | inline?: boolean; 5 | prettify?: boolean; 6 | children?: React.ReactNode | Record | undefined; 7 | }; 8 | 9 | export default function Code({ inline = false, prettify = true, className, children, ...props }: CodeProps) { 10 | let content: string; 11 | if (typeof children === 'object') content = JSON.stringify(children, null, 2); 12 | else content = children?.toString() ?? ''; 13 | 14 | if (inline) 15 | return ( 16 |
17 | {content} 18 |
19 | ); 20 | 21 | const finalContent = content && prettify ? prettyFormat(content) : content; 22 | const lines = finalContent.split('\n'); 23 | 24 | return ( 25 |
26 |
    27 | {lines.map((x, idx) => ( 28 |
  • {x}
  • 29 | ))} 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/webui/src/components/CodeDisplay.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render, waitFor } from '@testing-library/react'; 2 | 3 | import CodeDisplay from './CodeDisplay'; 4 | 5 | jest.mock( 6 | './MonacoEditor', 7 | () => 8 | function MockEditor({ value, language }: any) { 9 | return
{value}
; 10 | }, 11 | ); 12 | 13 | describe('CodeDisplay', () => { 14 | afterEach(() => { 15 | cleanup(); 16 | }); 17 | 18 | it('should parse application/json', async () => { 19 | const data = { foo: 'bar' }; 20 | const { getByTestId } = render(); 21 | 22 | const reactJson = await waitFor(() => getByTestId('lang-json')); 23 | expect(reactJson).toHaveTextContent('{ "foo": "bar" }'); 24 | }); 25 | 26 | it('should parse application/graphql-response+json as json', async () => { 27 | const data = { foo: 'bar' }; 28 | const { getByTestId } = render( 29 | , 30 | ); 31 | 32 | const reactJson = await waitFor(() => getByTestId('lang-json')); 33 | expect(reactJson).toHaveTextContent('{ "foo": "bar" }'); 34 | }); 35 | 36 | it('should parse application/json with charset', async () => { 37 | const data = { foo: 'bar' }; 38 | const { getByTestId } = render( 39 | , 40 | ); 41 | 42 | const reactJson = await waitFor(() => getByTestId('lang-json')); 43 | expect(reactJson).toHaveTextContent('{ "foo": "bar" }'); 44 | }); 45 | 46 | it('should use txt language when contentType is undefined', async () => { 47 | const data = { foo: 'bar' }; 48 | const { getByTestId } = render(); 49 | 50 | const reactJson = await waitFor(() => getByTestId('lang-txt')); 51 | expect(reactJson).toHaveTextContent('{"foo":"bar"}'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/webui/src/components/CodeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { safeParseJson } from '@envyjs/core'; 2 | import formatXml from 'xml-formatter'; 3 | 4 | import { tw } from '@/utils'; 5 | 6 | import Editor, { EditorHeight, MonacoEditorProps } from './MonacoEditor'; 7 | 8 | type CodeDisplayProps = { 9 | contentType?: string | string[] | null; 10 | data: string | null | undefined; 11 | editorHeight?: EditorHeight; 12 | className?: string; 13 | }; 14 | 15 | const languageMap: Record = { 16 | 'application/json': 'json', 17 | 'application/graphql-response+json': 'json', 18 | 'application/xml': 'xml', 19 | }; 20 | 21 | export default function CodeDisplay({ data, contentType, editorHeight, className }: CodeDisplayProps) { 22 | if (!data) { 23 | return; 24 | } 25 | 26 | // content types can be an array or a string value 27 | // each value in the array or string can be a content type with a charset 28 | // example: [content-type: application/json; charset=utf-8] 29 | let resolvedContentType = Array.isArray(contentType) ? contentType[0] : contentType; 30 | resolvedContentType = resolvedContentType && resolvedContentType.split(';')[0]; 31 | const lang = resolvedContentType ? languageMap[resolvedContentType as string] : 'txt'; 32 | 33 | let value = data; 34 | if (lang === 'json') { 35 | const parseResult = safeParseJson(data); 36 | if (parseResult.value) { 37 | value = JSON.stringify(parseResult.value, null, 2); 38 | } 39 | } else if (lang === 'xml') { 40 | value = formatXml(data, { 41 | indentation: ' ', 42 | lineSeparator: '\n', 43 | collapseContent: true, 44 | whiteSpaceAtEndOfSelfclosingTag: true, 45 | }); 46 | } 47 | 48 | return ( 49 |
50 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/webui/src/components/DarkModeToggle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import DarkModeToggle from './DarkModeToggle'; 4 | 5 | const meta = { 6 | title: 'Components/DarkModeToggle', 7 | component: DarkModeToggle, 8 | parameters: { 9 | layout: 'centered', 10 | }, 11 | argTypes: {}, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Standard: Story = { 18 | args: {}, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/webui/src/components/DarkModeToggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, cleanup, render } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import DarkModeToggle from './DarkModeToggle'; 5 | 6 | function isDarkModeOnRoot() { 7 | return document.documentElement.classList.contains('dark'); 8 | } 9 | 10 | describe('DarkModeToggle', () => { 11 | afterEach(() => { 12 | cleanup(); 13 | }); 14 | 15 | it('should render without error', () => { 16 | render(); 17 | }); 18 | 19 | it('should toggle the class on the root element', async () => { 20 | const isDarkMode = isDarkModeOnRoot(); 21 | 22 | const { getByRole } = render(); 23 | const toggle = getByRole('toggle'); 24 | 25 | // toggle once 26 | await act(async () => { 27 | await userEvent.click(toggle); 28 | }); 29 | 30 | expect(isDarkModeOnRoot()).toEqual(!isDarkMode); 31 | 32 | // toggle again 33 | await act(async () => { 34 | await userEvent.click(toggle); 35 | }); 36 | 37 | expect(isDarkModeOnRoot()).toEqual(isDarkMode); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/webui/src/components/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { MoonStar, SunMedium } from 'lucide-react'; 2 | import { useState } from 'react'; 3 | 4 | import Button from './Button'; 5 | 6 | export default function DarkModeToggle() { 7 | const initialTheme = localStorage.theme === 'dark'; 8 | const [useDarkMode, setUseDarkMode] = useState(initialTheme); 9 | 10 | const handleCheckboxChange = () => { 11 | const shouldSetDarkMode = !useDarkMode; 12 | 13 | if (shouldSetDarkMode) { 14 | document.documentElement.classList.add('dark'); 15 | localStorage.theme = 'dark'; 16 | } else { 17 | document.documentElement.classList.remove('dark'); 18 | localStorage.theme = 'light'; 19 | } 20 | 21 | setUseDarkMode(shouldSetDarkMode); 22 | }; 23 | 24 | const Icon = useDarkMode ? MoonStar : SunMedium; 25 | 26 | return 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/webui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | export { default as Authorization } from './Authorization'; 4 | export { default as Badge } from './Badge'; 5 | export { default as Button } from './Button'; 6 | export { default as Code } from './Code'; 7 | export { default as CodeDisplay } from './CodeDisplay'; 8 | export { default as DateTime } from './DateTime'; 9 | export { default as Fields, Field } from './Fields'; 10 | export { default as Input } from './Input'; 11 | export { default as KeyValueList } from './KeyValueList'; 12 | export { default as Loading } from './Loading'; 13 | export { default as Menu } from './Menu'; 14 | export { default as SearchInput } from './SearchInput'; 15 | export { default as Section } from './Section'; 16 | export { default as ToggleSwitch } from './ToggleSwitch'; 17 | -------------------------------------------------------------------------------- /packages/webui/src/components/ui/CopyAsCurlButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { HttpRequestState } from '@envyjs/core'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import CopyAsCurlButton from './CopyAsCurlButton'; 5 | 6 | const meta = { 7 | title: 'UI/CopyAsCurlButton', 8 | component: CopyAsCurlButton, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Standard: Story = { 18 | args: { 19 | trace: { 20 | id: 'http-1', 21 | timestamp: 1616239022, 22 | http: { 23 | host: 'httpbin.org', 24 | method: 'GET', 25 | port: 443, 26 | path: '/api/v1/?trace=123&name=test', 27 | requestHeaders: { 28 | 'content-type': 'application/json', 29 | 'x-custom-header': 'custom value', 30 | }, 31 | state: HttpRequestState.Received, 32 | url: 'https://httpbin.org/api/v1/?trace=123&name=test', 33 | }, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/webui/src/components/ui/CopyAsCurlButton.tsx: -------------------------------------------------------------------------------- 1 | import { safeParseJson } from '@envyjs/core'; 2 | import { CurlGenerator } from 'curl-generator'; 3 | import { ClipboardCopy } from 'lucide-react'; 4 | import { toast } from 'react-hot-toast'; 5 | 6 | import { Button } from '@/components'; 7 | import { Trace } from '@/types'; 8 | import { cloneHeaders, flatMapHeaders } from '@/utils'; 9 | 10 | type CopyAsCurlButtonProps = { 11 | trace: Trace; 12 | }; 13 | 14 | export default function CopyAsCurlButton({ trace, ...props }: CopyAsCurlButtonProps) { 15 | async function copyAsCurl() { 16 | const headers = flatMapHeaders(cloneHeaders(trace.http!.requestHeaders, true)); 17 | const body = safeParseJson(trace.http!.requestBody).value ?? null; 18 | 19 | const curlSnippet = CurlGenerator({ 20 | method: trace.http!.method as any, 21 | url: trace.http!.url, 22 | headers, 23 | body, 24 | }); 25 | 26 | await navigator.clipboard.writeText(curlSnippet); 27 | 28 | toast.success('cURL snippet written to clipboard', { 29 | position: 'top-right', 30 | }); 31 | } 32 | 33 | return