├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc.json ├── LICENSE ├── README.md ├── __tests__ └── server.test.ts ├── helpers ├── getUser.ts ├── handleLogOutHelper.ts └── validateStrongPassword.ts ├── index.html ├── jest.config.mjs ├── package-lock.json ├── package.json ├── package ├── README.md ├── collector-gateway-config.yaml ├── docker-compose.yaml ├── instrumentation.js ├── instrumentation.node.js ├── instrumentation.node.ts ├── instrumentation.ts ├── package.json ├── preInstall.js ├── prometheus.yaml └── src │ ├── index.edge.js │ ├── instrumentation.js │ └── instrumentation.node.js ├── postcss.config.mjs ├── public └── favicon.ico ├── server ├── controllers │ ├── appsController.ts │ ├── authenticateController.ts │ ├── pagesController.ts │ └── userController.ts ├── models │ └── dataModels.ts ├── routes │ ├── apiRouter.ts │ ├── appsRouter.ts │ ├── pagesRouter.ts │ └── userRouter.ts └── server.ts ├── src ├── App.tsx ├── assets │ ├── GitHub_Logo_White.webp │ ├── NV-logo-transparent.webp │ ├── NV-logo-white.webp │ ├── NextView-logo-pink-transparent.webp │ ├── NextView-logo-white-48x48.webp │ ├── Party_Popper_Emojipedia.webp │ ├── checkmark.png │ ├── copy.webp │ ├── eduardo.webp │ ├── evram.webp │ ├── github-mark-white.webp │ ├── github-mark.webp │ ├── hands.webp │ ├── kinski.webp │ ├── linked-in.png │ ├── npm-black.png │ ├── npm.webp │ ├── overview.webp │ ├── scott.webp │ ├── sooji.webp │ ├── star.webp │ ├── team.webp │ └── telescope.webp ├── components │ ├── Box.tsx │ ├── Button.tsx │ ├── CopyInput.tsx │ ├── Feature.tsx │ ├── NPMCopyInput.tsx │ ├── ProtectedRoute.tsx │ └── Spinner.tsx ├── contexts │ ├── dashboardContexts.tsx │ └── userContexts.tsx ├── index.css ├── main.tsx ├── pages │ ├── Dashboard │ │ ├── Dashboard.tsx │ │ ├── Loading.tsx │ │ ├── MainDisplay │ │ │ ├── MainDisplay.tsx │ │ │ ├── OverviewDisplay │ │ │ │ ├── BarGraph.tsx │ │ │ │ ├── HorizontalBarGraph.tsx │ │ │ │ ├── LineChart.tsx │ │ │ │ ├── OverviewDisplay.tsx │ │ │ │ └── Texbox.tsx │ │ │ ├── PageDisplay │ │ │ │ ├── Box.tsx │ │ │ │ ├── PageDisplay.tsx │ │ │ │ ├── PageLineChart.tsx │ │ │ │ ├── SpanLineChart.tsx │ │ │ │ ├── Table.tsx │ │ │ │ └── Textbox.tsx │ │ │ └── Topbar.tsx │ │ ├── Sidebar │ │ │ ├── MainNavbar.tsx │ │ │ ├── PageTab.tsx │ │ │ ├── SideNavbar.tsx │ │ │ └── Sidebar.tsx │ │ └── index.tsx │ ├── Home │ │ ├── Auth │ │ │ ├── AuthContainer.tsx │ │ │ ├── AuthForm.tsx │ │ │ ├── LoginForm.tsx │ │ │ ├── Modal.tsx │ │ │ └── SignupForm.tsx │ │ ├── Contributor.tsx │ │ ├── Contributors.tsx │ │ ├── Features.tsx │ │ ├── Installation.tsx │ │ ├── Navbar.tsx │ │ ├── Overview.tsx │ │ └── index.tsx │ └── NotFound │ │ └── NotFound.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types └── express │ └── index.d.ts └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "browser": true, "es2020": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, 5 | "plugins": ["@typescript-eslint", "react-refresh"], 6 | "root": true, 7 | "ignorePatterns": ["node_modules", "dist", "package"], 8 | "rules": { 9 | "react-refresh/only-export-components": "warn", 10 | "no-unused-vars": ["off", { "vars": "local" }], 11 | "@typescript-eslint/no-unused-vars": "off", 12 | "prefer-const": "warn" 13 | }, 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:react-hooks/recommended", 18 | "prettier" 19 | ], 20 | "settings": { 21 | "react": { 22 | "version": "detect" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main", "dev" ] 9 | pull_request: 10 | branches: [ "main", "dev" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | node_modules 7 | dist 8 | dist-ssr 9 | *.local 10 | .env 11 | .eslintcache 12 | 13 | # Editor directories and files 14 | .vscode/* 15 | !.vscode/extensions.json 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "plugins": ["prettier-plugin-tailwindcss"] 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![NextView-banner-final-900x300](https://github.com/oslabs-beta/NextView/assets/120596825/87836435-91c4-4081-9bdd-60e14af9dee5) 4 | 5 |
6 | 7 |
8 | 9 | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 10 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 11 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 12 | ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) 13 | ![MUI](https://img.shields.io/badge/Material%20UI-007FFF?style=for-the-badge&logo=mui&logoColor=white) 14 | ![Recharts](https://img.shields.io/badge/-1CA9C9?style=for-the-badge) 15 | ![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white) 16 | ![Express](https://img.shields.io/badge/express-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 17 | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) 18 | ![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-3d348b?style=for-the-badge&logo=opentelemetry&logoColor=white) 19 | ![NextJS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white) 20 | ![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 21 | ![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white) 22 | ![GitHubActions](https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) 23 | ![Husky](https://img.shields.io/badge/🐶husky-3EB489?style=for-the-badge) 24 | ![Jest](https://img.shields.io/badge/-jest-C21325?style=for-the-badge&logo=jest&logoColor=white) 25 | 26 |
27 | 28 | # 29 | 30 | ![Website](https://img.shields.io/badge/Website-B9D9EB) 31 | ![LinkedIn](https://img.shields.io/badge/LinkedIn-B9D9EB) 32 | ![npm](https://img.shields.io/badge/npm-B9D9EB) 33 | ![Medium](https://img.shields.io/badge/Medium-B9D9EB) 34 | 35 | NextView is a lightweight and user-friendly application designed to assist developers in optimizing the server performance of their Next.js applications. Our observability platform utilizes OpenTelemetry to trace and monitor crucial server metrics, stores the data in real time, and visualizes the time-series data in clear graphical representations on the NextView Dashboard. With easier data analysis, developers can swiftly identify bottlenecks and pinpoint areas that require server performance optimization, and thereby improve the efficiency of their applications. 36 | 37 | ## Getting Started 38 | 39 | 1. To get started, install our npm package in your Next.js application 40 | 41 | ```bash 42 | npm i nextview-tracing 43 | ``` 44 | 45 | 2. In your next.config.js file, opt-in to the Next.js instrumentation by setting the experimental instrumentationHook to true 46 | 47 | ```bash 48 | experimental.instrumentationHook = true; 49 | ``` 50 | 51 | 3. Navigate to the NextView Dashboard and copy your generated API key 52 | 53 | 4. In the .env.local file in the root directory of your application (create one if it doesn’t exist), create an environment variable for your API Key 54 | 55 | ```bash 56 | API_KEY= 57 | ``` 58 | 59 | 5. Return to your NextView account and enter the Dashboard to see the metrics displayed! 60 | 61 | ## Key Concepts in OpenTelemetry 62 | 63 | **Trace** 64 | 65 |

66 | The entire "path" of events that occurs when a request is made to an application. A trace is a collection of spans. 67 |

68 | 69 | **Span** 70 | 71 |

72 | A trace consists of spans, each of which represents an individual operation. A span contains information on the operation, such as request methods (get/post), start and end timestamps, status codes, and URL endpoints. NextView focuses on three main spans. 73 |

74 | 75 | - Client: The span is a request to some remote service, and does not complete until a response is received. It is usually the parent of a remote server span. 76 | - Server: The child of a remote client span that covers server-side handling of a remote request. 77 | - Internal: The span is an internal operation within an application that does not have remote parents or children. 78 | 79 | **Action** 80 | 81 |

82 | The term "action" in the NextView application refers to a child span within a trace. A single trace typically contains a parent span and one or more child spans. While the parent span represents the request to a particular page, the child spans represent the various actions that need to be completed before that request can be fulfilled. 83 |

84 | 85 | For more details on OpenTelemetry, please read the documentation [here](https://opentelemetry.io/docs/concepts/signals/). 86 | 87 | ## User Guidelines 88 | 89 | ### Overview Page 90 | 91 | ![dashboard-overview](https://github.com/oslabs-beta/NextView/assets/101832001/9f22cba0-3a6d-476d-8649-b9661c9688c4) 92 | 93 | The NextView Dashboard automatically lands the Overview page that provides an overview of performance metrics for your entire Next.js application. Specific values can be seen by hovering over the graph. 94 | 95 | Metrics displayed on the page include: 96 | 97 | - Average page load duration (in milliseconds) 98 | - Total number of traces 99 | - Average span load duration 100 | - Top 5 slowest pages 101 | - Average duration of operations by span kind (in milliseconds) over time 102 | 103 | By default, the overview data covers the last 24 hours. You can modify the time period using the date and time selector located in the top right corner of the dashboard. 104 | 105 | ### User's App Page(s) 106 | 107 | ![Screenshot 2023-06-21 at 1 26 38 PM](https://github.com/oslabs-beta/NextView/assets/101832001/d475373e-cc1d-4055-bdd5-069fb74b1b04) 108 | 109 | On the left-hand sidebar, you will find a list of all the pages in your application. When selecting a specific page, you can view server performance metrics for that individual page. 110 | 111 | Metrics displayed for each page include: 112 | 113 | - Average page load duration (in milliseconds) 114 | - Total number of traces 115 | - Details on each request (duration in milliseconds, number of traces, number of executions) 116 | - Average duration of actions (in milliseconds) over time 117 | 118 | ## Contribution Guidelines 119 | 120 | ### Contribution Method 121 | 122 | We welcome your contributions to the NextView product! 123 | 124 | 1. Fork the repo 125 | 2. Create your feature branch (`git checkout -b feature/newFeature`) and create your new feature 126 | 3. Commit your changes (`git commit -m 'Added [new-feature-description]'`) 127 | 4. Push to the branch (`git push origin feature/newFeature`) 128 | 5. Make a Pull Request 129 | 6. The NextView Team will review the feature and approve! 130 | 131 | ### Looking Ahead 132 | 133 | Here’s a list of features being considered by our team: 134 | 135 | - Enable multiple applications to be added to a single user account 136 | - Incorporate additional OpenTelemetry instrumentation (Metrics and Logs) to visualize on the dashboard 137 | - NextView is currently collecting observability metrics and allows for default visualization via Prometheus. To access metrics, users can spin up the NextView custom collector via Docker: `docker-compose up` which will automatically route all metrics data to Prometheus at the default endpoint of localhost:9090 138 | - Incorporate metrics visualization in our own dashboard moving forward 139 | - Enable user to select time zone 140 | - Enhance security through change password functionality 141 | - Add comprehensive testing suite 142 | - Add a dark mode feature 143 | 144 | ## Contributors 145 | 146 | - Eduardo Zayas: [GitHub](https://github.com/eza16) | [LinkedIn](https://www.linkedin.com/in/eduardo-zayas-avila/) 147 | - Evram Dawd: [GitHub](https://github.com/evramdawd) | [LinkedIn](https://www.linkedin.com/in/evram-d-905a3a2b/) 148 | - Kinski (Jiaxin) Wu: [GitHub](https://github.com/kinskiwu) | [LinkedIn](https://www.linkedin.com/in/kinskiwu/) 149 | - Scott Brasko: [GitHub](https://github.com/Scott-Brasko) | [LinkedIn](https://www.linkedin.com/in/scott-brasko/) 150 | - SooJi Kim: [GitHub](https://github.com/sjk06) | [LinkedIn](https://www.linkedin.com/in/sooji-suzy-kim/) 151 | 152 | ## License 153 | 154 | Distributed under the MIT License. See LICENSE for more information. 155 | -------------------------------------------------------------------------------- /__tests__/server.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../server/server'; 2 | import supertest from 'supertest'; 3 | 4 | const request = supertest(app); 5 | 6 | afterAll(() => app.close()); 7 | 8 | describe('/test endpoint', () => { 9 | it('should return a response', async () => { 10 | const response = await request.get('/test'); 11 | expect(response.status).toBe(200); 12 | expect(response.text).toBe('Hello world'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /helpers/getUser.ts: -------------------------------------------------------------------------------- 1 | import db from '../server/models/dataModels'; 2 | import { QueryResult } from 'pg'; 3 | 4 | interface User { 5 | _id: number; 6 | username: string; 7 | password: string; 8 | created_on: Date; 9 | } 10 | const getUser = async (username: string): Promise> => { 11 | // Get user with the given username 12 | const query = 'SELECT * FROM users WHERE username = $1'; 13 | const values = [username]; 14 | const user: QueryResult = await db.query(query, values); 15 | return user; 16 | }; 17 | 18 | export default getUser; 19 | -------------------------------------------------------------------------------- /helpers/handleLogOutHelper.ts: -------------------------------------------------------------------------------- 1 | const handleLogOutHelper = (setLoggedIn, navigate) => { 2 | fetch('/user/logout', { 3 | method: 'DELETE', 4 | headers: { 5 | 'Content-Type': 'Application/JSON', 6 | }, 7 | }) 8 | .then((res) => { 9 | if (res.status === 204) { 10 | localStorage.removeItem('user'); 11 | setLoggedIn(false); 12 | navigate('/'); 13 | window.location.reload(); 14 | } else { 15 | alert('Logout unsuccessful. Please retry.'); 16 | } 17 | }) 18 | .catch((err) => console.log('Logout ERROR: ', err)); 19 | }; 20 | 21 | export default handleLogOutHelper; 22 | -------------------------------------------------------------------------------- /helpers/validateStrongPassword.ts: -------------------------------------------------------------------------------- 1 | const validateStrongPassword = (password: string): boolean => { 2 | const scores = { 3 | length: 0, 4 | upperChar: 0, 5 | lowerChar: 0, 6 | number: 0, 7 | specialChar: 0, 8 | }; 9 | 10 | for (const char of password) { 11 | // convert char into ASCII 12 | const charASCII = char.charCodeAt(0); 13 | 14 | // A-Z: 65 - 90 15 | if (charASCII > 64 && charASCII < 91) { 16 | scores.upperChar += 1; 17 | // a-z: 97 - 122 18 | } else if (charASCII > 96 && charASCII < 123) { 19 | scores.lowerChar += 1; 20 | // 0 - 9: 48-57 21 | } else if (charASCII > 47 && charASCII < 58) { 22 | scores.number += 1; 23 | // s"#$%&'()*+,-./ : 33-47, :;<=>?@: 58 - 64, [\]^_` : 91-96, {|}~.: 123 - 126 24 | } else if ( 25 | (charASCII > 32 && charASCII < 48) || 26 | (charASCII > 57 && charASCII < 65) || 27 | (charASCII > 90 && charASCII < 97) || 28 | (charASCII > 122 && charASCII < 127) 29 | ) { 30 | scores.specialChar += 1; 31 | } 32 | scores.length += 1; 33 | } 34 | 35 | return scores.length < 8 || 36 | scores.upperChar < 1 || 37 | scores.lowerChar < 1 || 38 | scores.number < 1 || 39 | scores.specialChar < 1 40 | ? false 41 | : true; 42 | }; 43 | 44 | export default validateStrongPassword; 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | NextView | Next.js Platform 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextview", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development concurrently -n BROWSER,SERVER -c bgBlue.bold,bgCyan.bold \"sleep 1 && vite --open\" \"nodemon server/server.ts\"", 7 | "build": "cross-env NODE_ENV=production vite build", 8 | "start": "cross-env NODE_ENV=production tsx server/server.ts", 9 | "lint": "eslint .", 10 | "prettierConflictCheck": "eslint-config-prettier src/main.tsx", 11 | "prepare": "husky install", 12 | "test": "cross-env NODE_ENV=test jest" 13 | }, 14 | "dependencies": { 15 | "@emotion/react": "^11.11.1", 16 | "@emotion/styled": "^11.11.0", 17 | "@mui/material": "^5.13.5", 18 | "@mui/x-date-pickers": "^6.7.0", 19 | "bcryptjs": "^2.4.3", 20 | "concurrently": "^8.0.1", 21 | "cookie-parser": "^1.4.6", 22 | "cross-env": "^7.0.3", 23 | "dayjs": "^1.11.8", 24 | "dotenv": "^16.1.4", 25 | "express": "^4.18.2", 26 | "jsonwebtoken": "^9.0.0", 27 | "pg": "^8.11.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-icons": "^4.9.0", 31 | "react-router-dom": "^6.11.2", 32 | "react-type-animation": "^3.1.0", 33 | "recharts": "^2.6.2", 34 | "tsx": "^3.12.7", 35 | "uuid": "^9.0.0" 36 | }, 37 | "devDependencies": { 38 | "@types/bcryptjs": "^2.4.2", 39 | "@types/cookie-parser": "^1.4.3", 40 | "@types/express": "^4.17.17", 41 | "@types/jest": "^29.5.2", 42 | "@types/jsonwebtoken": "^9.0.2", 43 | "@types/node": "^20.2.5", 44 | "@types/pg": "^8.10.1", 45 | "@types/react": "^18.0.37", 46 | "@types/react-dom": "^18.0.11", 47 | "@types/supertest": "^2.0.12", 48 | "@types/uuid": "^9.0.1", 49 | "@typescript-eslint/eslint-plugin": "^5.59.0", 50 | "@typescript-eslint/parser": "^5.59.0", 51 | "@vitejs/plugin-react": "^4.0.0", 52 | "autoprefixer": "^10.4.14", 53 | "eslint": "^8.38.0", 54 | "eslint-config-prettier": "^8.8.0", 55 | "eslint-plugin-react": "^7.32.2", 56 | "eslint-plugin-react-hooks": "^4.6.0", 57 | "eslint-plugin-react-refresh": "^0.3.4", 58 | "husky": "^8.0.3", 59 | "jest": "^29.5.0", 60 | "lint-staged": "^13.2.2", 61 | "nodemon": "^2.0.22", 62 | "prettier": "^2.8.8", 63 | "prettier-plugin-tailwindcss": "^0.3.0", 64 | "rollup-plugin-gzip": "^3.1.0", 65 | "supertest": "^6.3.3", 66 | "tailwindcss": "^3.3.2", 67 | "ts-jest": "^29.1.0", 68 | "typescript": "^5.0.2", 69 | "vite": "^4.3.9" 70 | }, 71 | "lint-staged": { 72 | "*.{js,ts,tsx,jsx}": "eslint --cache --fix", 73 | "*.{js,css,md,ts,tsx,jsx}": "prettier --write" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 | # PACKAGE 2 | 3 | ![NextView-banner-final-900x300](https://github.com/oslabs-beta/NextView/assets/101832001/fd3242b4-3af5-42ea-96ff-81b288ef8c66) 4 | 5 |
6 | 7 | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 8 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 9 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 10 | ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) 11 | ![MUI](https://img.shields.io/badge/Material%20UI-007FFF?style=for-the-badge&logo=mui&logoColor=white) 12 | ![Recharts](https://img.shields.io/badge/-1CA9C9?style=for-the-badge) 13 | ![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white) 14 | ![Express](https://img.shields.io/badge/express-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 15 | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) 16 | ![OpenTelemetry](https://img.shields.io/badge/OpenTelemetry-3d348b?style=for-the-badge&logo=opentelemetry&logoColor=white) 17 | ![NextJS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white) 18 | ![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 19 | ![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white) 20 | ![GitHubActions](https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) 21 | ![Husky](https://img.shields.io/badge/🐶husky-3EB489?style=for-the-badge) 22 | ![Jest](https://img.shields.io/badge/-jest-C21325?style=for-the-badge&logo=jest&logoColor=white) 23 | 24 |
25 | 26 | # 27 | 28 | ![Website](https://img.shields.io/badge/Website-B9D9EB) 29 | ![LinkedIn](https://img.shields.io/badge/LinkedIn-B9D9EB) 30 | ![npm](https://img.shields.io/badge/npm-B9D9EB) 31 | ![Medium](https://img.shields.io/badge/Medium-B9D9EB) 32 | 33 | NextView is a lightweight and user-friendly application designed to assist developers in optimizing the server performance of their Next.js applications. Our observability platform utilizes OpenTelemetry to trace and monitor crucial server instrumentation data, stores the information in real time, and visualizes the time-series data in clear graphical representations on the NextView Dashboard. With easier data analysis, developers can swiftly identify bottlenecks and pinpoint areas that require server performance optimization, and thereby improve the efficiency of their applications. 34 | 35 | ## Getting Started 36 | 37 | 1. To get started, install our npm package in your Next.js application 38 | 39 | ```bash 40 | npm i nextview-tracing 41 | ``` 42 | 43 | 2. In your next.config.js file, opt-in to the Next.js instrumentation by setting the experimental instrumentationHook key to true in the nextConfig object 44 | 45 | ```bash 46 | experimental.instrumentationHook = true; 47 | ``` 48 | 49 | 3. Navigate to the NextView Dashboard and copy your generated API key 50 | 51 | 4. In the .env.local file in the root directory of your application (create one if it doesn’t exist), create two environment variables, one for your API Key and one for your service’s name 52 | 53 | ```bash 54 | API_KEY= 55 | Service_Name= 56 | ``` 57 | 58 | 5. Start the OpenTelemetry Collector in your terminal via the Docker Command 59 | 60 | ```bash 61 | docker-compose-up 62 | ``` 63 | 64 | 6. Return to your NextView account and enter the Dashboard to see your instrumentation data displayed! 65 | 66 | ## Key Concepts in OpenTelemetry 67 | 68 | **Trace** 69 | 70 |

71 | The entire "path" of events that occurs when a request is made to an application. A trace is a collection of spans. 72 |

73 | 74 | **Span** 75 | 76 |

77 | A trace consists of spans, each of which represents an individual operation. A span contains information on the operation, such as request methods (get/post), start and end timestamps, status codes, and URL endpoints. NextView focuses on three main spans. 78 |

79 | 80 | - Client: The span is a request to some remote service, and does not complete until a response is received. It is usually the parent of a remote server span. 81 | - Server: The child of a remote client span that covers server-side handling of a remote request. 82 | - Internal: The span is an internal operation within an application that does not have remote parents or children. 83 | 84 | **Action** 85 | 86 |

87 | The term "action" in the NextView application refers to one or more operations (spans) within a trace with the same request method and URL endpoint. 88 |

89 | 90 | For more details on OpenTelemetry, please read the documentation [here](https://opentelemetry.io/docs/concepts/signals/). 91 | 92 | ## Contributors 93 | 94 | - Eduardo Zayas: [GitHub](https://github.com/eza16) | [LinkedIn](https://www.linkedin.com/in/eduardo-zayas-avila/) 95 | - Evram Dawd: [GitHub](https://github.com/evramdawd) | [LinkedIn](https://www.linkedin.com/in/evram-d-905a3a2b/) 96 | - Kinski (Jiaxin) Wu: [GitHub](https://github.com/kinskiwu) | [LinkedIn](https://www.linkedin.com/in/kinskiwu/) 97 | - Scott Brasko: [GitHub](https://github.com/Scott-Brasko) | [LinkedIn](https://www.linkedin.com/in/scott-brasko/) 98 | - SooJi Kim: [GitHub](https://github.com/sjk06) | [LinkedIn](https://www.linkedin.com/in/sooji-suzy-kim/) 99 | 100 | ## License 101 | 102 | Distributed under the MIT License. See LICENSE for more information. 103 | -------------------------------------------------------------------------------- /package/collector-gateway-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: #HTTP or gRPC 4 | http: 5 | endpoint: 0.0.0.0:4318 6 | grpc: 7 | endpoint: 0.0.0.0:4317 8 | 9 | exporters: # EXPORTERS 10 | prometheus: # For Metrics 11 | endpoint: '0.0.0.0:8889' 12 | send_timestamps: true 13 | namespace: promexample 14 | const_labels: 15 | label1: value1 16 | 17 | logging: # Logging to console 18 | # loglevel: DEBUG 19 | verbosity: detailed 20 | sampling_initial: 5 21 | sampling_thereafter: 200 22 | 23 | # zipkin: 24 | # endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" 25 | # format: proto 26 | 27 | # jaeger: 28 | # endpoint: jaeger-all-in-one:14250 29 | # tls: 30 | # insecure: true 31 | 32 | processors: # PROCESSORS 33 | batch: 34 | timeout: 5s # in production, this will be > 5s for sure 35 | resource: # alters the data that will be sent! Adds attribute of test key & value 36 | attributes: 37 | - key: NextView 38 | value: 'Tracing/Metrics' 39 | action: insert # insert, upsert, delete - see documentation 40 | 41 | extensions: # EXTENSIONS 42 | health_check: 43 | pprof: 44 | endpoint: :1888 45 | zpages: 46 | endpoint: :55679 47 | 48 | service: # SERVICES 49 | extensions: [pprof, zpages, health_check] 50 | pipelines: # 2 pipelines: 51 | traces: 52 | receivers: [otlp] 53 | processors: [batch, resource] 54 | exporters: [logging] 55 | metrics: 56 | receivers: [otlp] 57 | processors: [batch] 58 | exporters: [logging, prometheus] 59 | -------------------------------------------------------------------------------- /package/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | # Could switch back to Version 2 if something breaks. Also, not specifying minor version here which might be best practice. 3 | services: 4 | # Jaeger 5 | # jaeger-all-in-one: 6 | # image: jaegertracing/all-in-one:latest 7 | # restart: always 8 | # ports: 9 | # - "16686:16686" 10 | # - "14268" 11 | # - "14250" 12 | 13 | # Zipkin 14 | # zipkin-all-in-one: 15 | # image: openzipkin/zipkin:latest 16 | # restart: always 17 | # ports: 18 | # - "9411:9411" 19 | 20 | # Collector 21 | otel-collector: 22 | # image: ${OTELCOL_IMG} 23 | # image: otel/opentelemetry-collector:0.67.0 24 | # image sourced from OTel docs (collector/getting-started) 25 | image: otel/opentelemetry-collector-contrib:0.76.1 26 | container_name: otel-col 27 | restart: always 28 | volumes: 29 | - ./collector-gateway-config.yaml:/etc/otel-collector-config.yaml 30 | command: ["--config=/etc/otel-collector-config.yaml"] 31 | ports: 32 | - "1888:1888" # pprof extension 33 | - "8888:8888" # Prometheus metrics exposed by the collector 34 | - "8889:8889" # Prometheus exporter metrics 35 | - "13133:13133" # health_check extension 36 | - "4317:4317" # OTLP gRPC receiver 37 | - "4318:4318" # OTLP HTTP receiver 38 | - "55679:55679" # zpages extension 39 | # depends_on: 40 | #- jaeger-all-in-one 41 | # - zipkin-all-in-one 42 | 43 | prometheus: 44 | container_name: prometheus 45 | image: prom/prometheus:latest 46 | restart: always 47 | volumes: 48 | - ./prometheus.yaml:/etc/prometheus/prometheus.yml 49 | ports: 50 | - "9090:9090" -------------------------------------------------------------------------------- /package/instrumentation.js: -------------------------------------------------------------------------------- 1 | import { nextView } from 'nextview-tracing'; 2 | 3 | export function register() { 4 | if (process.env.NEXT_RUNTIME === 'nodejs') { 5 | nextView('next-app!!'); 6 | } 7 | } 8 | 9 | // export async function register() { 10 | // if (process.env.NEXT_RUNTIME === 'nodejs') { 11 | // await import('./instrumentation.node.ts'); 12 | // } 13 | // } 14 | -------------------------------------------------------------------------------- /package/instrumentation.node.js: -------------------------------------------------------------------------------- 1 | // import { trace, context } from '@opentelemetry/api'; 2 | import { NodeSDK } from '@opentelemetry/sdk-node'; 3 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; 4 | import { Resource } from '@opentelemetry/resources'; 5 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 6 | import { 7 | SimpleSpanProcessor, 8 | ConsoleSpanExporter, 9 | ParentBasedSampler, 10 | TraceIdRatioBasedSampler, 11 | Span, 12 | } from '@opentelemetry/sdk-trace-node'; 13 | import { IncomingMessage } from 'http'; 14 | 15 | // ADDITIONAL INSTRUMENTATION: 16 | import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; 17 | // const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); 18 | 19 | import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; 20 | 21 | // Trying to convert the CommonJS "require" statements below to ES6 "import" statements: 22 | import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; 23 | import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; 24 | import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; 25 | import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; 26 | 27 | // CommonJS Require Statements causing issues when trying to implement Vercel style wrapping. 28 | // const { 29 | // ExpressInstrumentation, 30 | // } = require('@opentelemetry/instrumentation-express'); 31 | // const { 32 | // MongooseInstrumentation, 33 | // } = require('@opentelemetry/instrumentation-mongoose'); 34 | // //pg instrumentation 35 | // const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg'); 36 | // const { 37 | // MongoDBInstrumentation, 38 | // } = require('@opentelemetry/instrumentation-mongodb'); 39 | 40 | export const nextView = (serviceName) => { 41 | const sdk = new NodeSDK({ 42 | resource: new Resource({ 43 | [SemanticResourceAttributes.SERVICE_NAME]: 'next-app', 44 | }), 45 | // spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()), 46 | spanProcessor: new SimpleSpanProcessor( 47 | new OTLPTraceExporter({ 48 | url: 'http://localhost:4318/v1/trace', 49 | // same port as shown in collector-gateway.yml 50 | headers: { 51 | foo: 'bar', 52 | }, // an optional object containing custom headers to be sent with each request will only work with http 53 | }), 54 | ), 55 | sampler: new ParentBasedSampler({ 56 | root: new TraceIdRatioBasedSampler(1), 57 | }), 58 | instrumentations: [ 59 | new HttpInstrumentation({ 60 | requestHook: (span, reqInfo) => { 61 | span.setAttribute('request-headers', JSON.stringify(reqInfo)); 62 | }, 63 | responseHook: (span, res) => { 64 | // Get 'content-length' size: 65 | let size = 0; 66 | res.on('data', (chunk) => { 67 | size += chunk.length; 68 | }); 69 | 70 | res.on('end', () => { 71 | span.setAttribute('contentLength', size); 72 | }); 73 | }, 74 | }), 75 | new ExpressInstrumentation({ 76 | // Custom Attribute: request headers on spans: 77 | requestHook: (span, reqInfo) => { 78 | span.setAttribute( 79 | 'request-headers', 80 | JSON.stringify(reqInfo.request.headers), 81 | ); // Can't get the right type for reqInfo here. Something to do with not being able to import instrumentation-express 82 | }, 83 | }), 84 | new MongooseInstrumentation({ 85 | // responseHook: (span: Span, res: { response: any }) => { 86 | responseHook: (span, res) => { 87 | span.setAttribute( 88 | 'contentLength', 89 | Buffer.byteLength(JSON.stringify(res.response)), 90 | ); 91 | span.setAttribute( 92 | 'instrumentationLibrary', 93 | span.instrumentationLibrary.name, 94 | ); 95 | }, 96 | }), 97 | // new PgInstrumentation({ 98 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 99 | // span.setAttribute("contentLength", Buffer.byteLength(JSON.stringify(res.data.rows))); 100 | // span.setAttribute("instrumentationLibrary", span.instrumentationLibrary.name); 101 | // }, 102 | // }), 103 | // new MongoDBInstrumentation({ 104 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 105 | // span.setAttribute("contentLength", Buffer.byteLength(JSON.stringify(res.data.rows))); 106 | // span.setAttribute("instrumentationLibrary", span.instrumentationLibrary.name); 107 | // }, 108 | // }), 109 | ], 110 | }); 111 | sdk.start(); 112 | }; 113 | -------------------------------------------------------------------------------- /package/instrumentation.node.ts: -------------------------------------------------------------------------------- 1 | // import { trace, context } from '@opentelemetry/api'; 2 | import { NodeSDK } from '@opentelemetry/sdk-node'; 3 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; 4 | import { Resource } from '@opentelemetry/resources'; 5 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 6 | import { 7 | SimpleSpanProcessor, 8 | ConsoleSpanExporter, 9 | ParentBasedSampler, 10 | TraceIdRatioBasedSampler, 11 | Span, 12 | } from '@opentelemetry/sdk-trace-node'; 13 | import { IncomingMessage } from 'http'; 14 | 15 | // ADDITIONAL INSTRUMENTATION: 16 | import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; 17 | // const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); 18 | 19 | // import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; 20 | const { 21 | ExpressInstrumentation, 22 | } = require('@opentelemetry/instrumentation-express'); 23 | const { 24 | MongooseInstrumentation, 25 | } = require('@opentelemetry/instrumentation-mongoose'); 26 | //pg instrumentation 27 | const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg'); 28 | const { 29 | MongoDBInstrumentation, 30 | } = require('@opentelemetry/instrumentation-mongodb'); 31 | 32 | const sdk = new NodeSDK({ 33 | resource: new Resource({ 34 | [SemanticResourceAttributes.SERVICE_NAME]: 'next-app', 35 | }), 36 | // spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()), 37 | spanProcessor: new SimpleSpanProcessor( 38 | new OTLPTraceExporter({ 39 | url: 'http://localhost:4318/v1/trace', 40 | // same port as shown in collector-gateway.yml 41 | headers: { 42 | foo: 'bar', 43 | }, // an optional object containing custom headers to be sent with each request will only work with http 44 | }), 45 | ), 46 | sampler: new ParentBasedSampler({ 47 | root: new TraceIdRatioBasedSampler(1), 48 | }), 49 | instrumentations: [ 50 | new HttpInstrumentation({ 51 | requestHook: (span, reqInfo) => { 52 | span.setAttribute('request-headers', JSON.stringify(reqInfo)); 53 | }, 54 | // responseHook: (span, res) => { 55 | // // Get 'content-length' size: 56 | // let size = 0; 57 | // res.on('data', (chunk) => { 58 | // size += chunk.length; 59 | // }); 60 | 61 | // res.on('end', () => { 62 | // span.setAttribute('contentLength', size) 63 | // }); 64 | // } 65 | }), 66 | new ExpressInstrumentation({ 67 | // Custom Attribute: request headers on spans: 68 | requestHook: (span: Span, reqInfo: any) => { 69 | span.setAttribute( 70 | 'request-headers', 71 | JSON.stringify(reqInfo.request.headers), 72 | ); // Can't get the right type for reqInfo here. Something to do with not being able to import instrumentation-express 73 | }, 74 | }), 75 | new MongooseInstrumentation({ 76 | responseHook: (span: Span, res: { response: any }) => { 77 | span.setAttribute( 78 | 'contentLength', 79 | Buffer.byteLength(JSON.stringify(res.response)), 80 | ); 81 | span.setAttribute( 82 | 'instrumentationLibrary', 83 | span.instrumentationLibrary.name, 84 | ); 85 | }, 86 | }), 87 | // new PgInstrumentation({ 88 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 89 | // span.setAttribute("contentLength", Buffer.byteLength(JSON.stringify(res.data.rows))); 90 | // span.setAttribute("instrumentationLibrary", span.instrumentationLibrary.name); 91 | // }, 92 | // }), 93 | // new MongoDBInstrumentation({ 94 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 95 | // span.setAttribute("contentLength", Buffer.byteLength(JSON.stringify(res.data.rows))); 96 | // span.setAttribute("instrumentationLibrary", span.instrumentationLibrary.name); 97 | // }, 98 | // }), 99 | ], 100 | }); 101 | sdk.start(); 102 | -------------------------------------------------------------------------------- /package/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === 'nodejs') { 3 | await import('./instrumentation.node.ts'); 4 | } 5 | } -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextview-tracing", 3 | "version": "1.1.1", 4 | "description": "OpenTelemetry Tracing Package for Next.js Applications", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "node -e 'fs.copyFileSync(path.join(__dirname, \"./src/instrumentation.js\"), path.join(path.resolve(__dirname, \"../..\"), \"instrumentation.js\"))'" 9 | }, 10 | "files": [ 11 | "src" 12 | ], 13 | "exports": { 14 | ".": { 15 | "edge": { 16 | "default": "./src/index.edge.js" 17 | }, 18 | "edge-light": { 19 | "default": "./src/index.edge.js" 20 | }, 21 | "browser": { 22 | "default": "./src/index.edge.js" 23 | }, 24 | "worker": { 25 | "default": "./src/index.edge.js" 26 | }, 27 | "workerd": { 28 | "default": "./src/index.edge.js" 29 | }, 30 | "import": { 31 | "default": "./src/instrumentation.node.js" 32 | }, 33 | "node": { 34 | "default": "./src/instrumentation.node.js" 35 | }, 36 | "default": "./src/index.edge.js" 37 | } 38 | }, 39 | "devDependencies": { 40 | "@types/node": "18.16.1", 41 | "typescript": "^5.0.4" 42 | }, 43 | "dependencies": { 44 | "@opentelemetry/exporter-trace-otlp-http": "^0.40.0", 45 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.39.1", 46 | "@opentelemetry/instrumentation": "^0.40.0", 47 | "@opentelemetry/instrumentation-express": "^0.32.3", 48 | "@opentelemetry/instrumentation-http": "^0.40.0", 49 | "@opentelemetry/instrumentation-mongoose": "^0.32.3", 50 | "@opentelemetry/instrumentation-pg": "^0.35.2", 51 | "@opentelemetry/resources": "^1.14.0", 52 | "@opentelemetry/sdk-node": "^0.40.0", 53 | "@opentelemetry/instrumentation-mongodb": "^0.34.2", 54 | "@opentelemetry/sdk-trace-node": "^1.14.0", 55 | "@opentelemetry/semantic-conventions": "^1.14.0", 56 | "@types/node": "20.2.5", 57 | "@types/react": "18.2.8", 58 | "@types/react-dom": "18.2.4", 59 | "ts-node": "^10.9.1", 60 | "ts-node-dev": "^2.0.0", 61 | "typescript": "5.1.3" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "git+https://github.com/oslabs-beta/NextView.git#main" 66 | }, 67 | "keywords": [ 68 | "next.js", 69 | "ssr", 70 | "OpenTelemetry", 71 | "instrumentation", 72 | "observability", 73 | "metrics", 74 | "traces" 75 | ], 76 | "author": "NextView", 77 | "license": "ISC", 78 | "bugs": { 79 | "url": "https://github.com/oslabs-beta/NextView/issues" 80 | }, 81 | "homepage": "https://github.com/oslabs-beta/NextView/tree/main#readme", 82 | "engines": { 83 | "node": ">=16" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package/preInstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | console.log('executing preInstall.js \n'); 5 | console.log('process.cwd():', process.cwd(), '\n'); // YOU WANT THIS ONE!! PROCESS.CWD(); 6 | console.log('__dirname/../..', path.resolve(__dirname, '../..')); 7 | console.log('__dirname:', __dirname, '\n'); 8 | console.log( 9 | 'path.resolve(__dirname, "/instrumentation.ts":', 10 | path.resolve(__dirname, './instrumentation.ts'), 11 | '\n', 12 | ); 13 | console.log( 14 | 'path.resolve(process.cwd(), "/instrumentation.ts":', 15 | path.resolve(process.cwd(), './node_modules/nextview_tracer'), 16 | ); 17 | console.log('************************'); 18 | 19 | // Copy instrumentation.ts: 20 | fs.copyFileSync( 21 | path.join(__dirname, 'instrumentation.ts'), 22 | path.join(path.resolve(__dirname, '../..'), 'instrumentation.ts'), 23 | ); 24 | 25 | // Copy instrumentation.node.ts: 26 | // fs.copyFileSync(path.join(__dirname, 'instrumentation.node.ts'), path.join(path.resolve(__dirname, '../..'), 'instrumentation.node.ts')); 27 | 28 | // Copy docker-compose.yaml: 29 | // fs.copyFileSync(path.join(__dirname, 'docker-compose.yaml'), path.join(path.resolve(__dirname, '../..'), 'docker-compose.yaml')); 30 | 31 | // Copy collector-gateway-config.yaml: 32 | // fs.copyFileSync(path.join(__dirname, 'collector-gateway-config.yaml'), path.join(path.resolve(__dirname, '../..'), 'collector-gateway-config.yaml')); 33 | 34 | // Copy prometheus.yaml: 35 | // fs.copyFileSync(path.join(__dirname, 'prometheus.yaml'), path.join(path.resolve(__dirname, '../..'), 'prometheus.yaml')); 36 | 37 | //*********************/ 38 | // fs.copyFile(__dirname + '/tracer.ts', process.cwd(), (err) => { 39 | // if(err) throw err; 40 | // console.log('File was copied to destination'); 41 | // }); 42 | -------------------------------------------------------------------------------- /package/prometheus.yaml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: "otel-collector" 3 | scrape_interval: 10s 4 | static_configs: 5 | - targets: ["otel-collector:8889"] 6 | - targets: ["otel-collector:8888"] 7 | -------------------------------------------------------------------------------- /package/src/index.edge.js: -------------------------------------------------------------------------------- 1 | export const registerOTel = (serviceName) => { 2 | // We don't support OTel on edge yet 3 | void serviceName; 4 | }; 5 | -------------------------------------------------------------------------------- /package/src/instrumentation.js: -------------------------------------------------------------------------------- 1 | import { nextView } from 'nextview-tracing'; 2 | 3 | export function register() { 4 | if (process.env.NEXT_RUNTIME === 'nodejs') { 5 | nextView('next-app!!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package/src/instrumentation.node.js: -------------------------------------------------------------------------------- 1 | import { trace, context } from '@opentelemetry/api'; 2 | import { NodeSDK } from '@opentelemetry/sdk-node'; 3 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; 4 | import { Resource } from '@opentelemetry/resources'; 5 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 6 | import { 7 | SimpleSpanProcessor, 8 | ConsoleSpanExporter, 9 | ParentBasedSampler, 10 | TraceIdRatioBasedSampler, 11 | Span, 12 | } from '@opentelemetry/sdk-trace-node'; 13 | import { IncomingMessage } from 'http'; 14 | 15 | // ADDITIONAL INSTRUMENTATION: 16 | import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; 17 | // const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); 18 | 19 | // Trying to convert the CommonJS "require" statements below to ES6 "import" statements: 20 | import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; 21 | import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; 22 | import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; 23 | import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; 24 | 25 | export const nextView = (serviceName) => { 26 | const collectorOptions = { 27 | url: 'http://www.nextview.dev/api', 28 | headers: { 29 | API_KEY: `${process.env.API_KEY}`, 30 | NextView: 'Next.js Tracing Information', 31 | // an optional object containing custom headers to be sent with each request will only work with http 32 | }, 33 | // concurrencyLimit: 10, // an optional limit on pending requests 34 | }; 35 | 36 | const sdk = new NodeSDK({ 37 | resource: new Resource({ 38 | [SemanticResourceAttributes.SERVICE_NAME]: `${process.env.Service_Name}`, 39 | }), 40 | // spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()), 41 | spanProcessor: new SimpleSpanProcessor( 42 | new OTLPTraceExporter(collectorOptions), 43 | ), 44 | sampler: new ParentBasedSampler({ 45 | root: new TraceIdRatioBasedSampler(1), 46 | }), 47 | instrumentations: [ 48 | new HttpInstrumentation({ 49 | requestHook: (span, reqInfo) => { 50 | span.setAttribute('request-headers', JSON.stringify(reqInfo)); 51 | }, 52 | // responseHook: (span, res) => { 53 | // // Get 'content-length' size: 54 | // let size = 0; 55 | // res.on('data', (chunk) => { 56 | // size += chunk.length; 57 | // }); 58 | 59 | // res.on('end', () => { 60 | // span.setAttribute('contentLength', size); 61 | // }); 62 | // }, 63 | }), 64 | new ExpressInstrumentation({ 65 | // Custom Attribute: request headers on spans: 66 | requestHook: (span, reqInfo) => { 67 | span.setAttribute( 68 | 'request-headers', 69 | JSON.stringify(reqInfo.request.headers), 70 | ); // Can't get the right type for reqInfo here. Something to do with not being able to import instrumentation-express 71 | }, 72 | }), 73 | new MongooseInstrumentation({ 74 | // responseHook: (span: Span, res: { response: any }) => { 75 | responseHook: (span, res) => { 76 | span.setAttribute( 77 | 'contentLength', 78 | Buffer.byteLength(JSON.stringify(res.response)), 79 | ); 80 | span.setAttribute( 81 | 'instrumentationLibrary', 82 | span.instrumentationLibrary.name, 83 | ); 84 | }, 85 | }), 86 | new PgInstrumentation({ 87 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 88 | responseHook: (span, res) => { 89 | span.setAttribute( 90 | 'contentLength', 91 | Buffer.byteLength(JSON.stringify(res.data.rows)), 92 | ); 93 | span.setAttribute( 94 | 'instrumentationLibrary', 95 | span.instrumentationLibrary.name, 96 | ); 97 | }, 98 | }), 99 | new MongoDBInstrumentation({ 100 | // responseHook: (span: Span, res: { data: { rows: any; }; }) => { 101 | responseHook: (span, res) => { 102 | span.setAttribute( 103 | 'contentLength', 104 | Buffer.byteLength(JSON.stringify(res.data.rows)), 105 | ); 106 | span.setAttribute( 107 | 'instrumentationLibrary', 108 | span.instrumentationLibrary.name, 109 | ); 110 | }, 111 | }), 112 | ], 113 | }); 114 | sdk.start(); 115 | }; 116 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/public/favicon.ico -------------------------------------------------------------------------------- /server/controllers/appsController.ts: -------------------------------------------------------------------------------- 1 | import db from '../models/dataModels'; 2 | import { v4 as uuid } from 'uuid'; 3 | import { RequestHandler } from 'express'; 4 | 5 | const appsController: AppsController = { 6 | createApiKey: (req, res, next) => { 7 | res.locals.apiKey = uuid(); 8 | return next(); 9 | }, 10 | 11 | registerApp: async (req, res, next) => { 12 | if (!res.locals.apiKey) 13 | return next({ 14 | log: `Error in registerApp controller method: No api key was generated`, 15 | status: 500, 16 | message: { err: 'No api key was generated' }, 17 | }); 18 | 19 | try { 20 | const text = 21 | 'INSERT INTO apps(id, user_id, app_name) VALUES($1, $2, $3) RETURNING *'; 22 | const values = [ 23 | res.locals.apiKey, 24 | req.user.userId, 25 | req.body.app_name || null, 26 | ]; 27 | const newApp = await db.query(text, values); 28 | res.locals.app = newApp.rows[0]; 29 | 30 | return next(); 31 | } catch (err) { 32 | // If an error occurs, pass it to the error handling middleware 33 | return next({ 34 | log: `Error in registerApp controller method: ${err}`, 35 | status: 500, 36 | message: 'Error while creating app', 37 | }); 38 | } 39 | }, 40 | 41 | retrieveApps: async (req, res, next) => { 42 | try { 43 | const text = 'SELECT * FROM apps WHERE user_id = $1'; 44 | const values = [req.user.userId]; 45 | const apps = await db.query(text, values); 46 | res.locals.apps = apps.rows; 47 | 48 | return next(); 49 | } catch (err) { 50 | // If an error occurs, pass it to the error handling middleware 51 | return next({ 52 | log: `Error in retrieveApps controller method: ${err}`, 53 | status: 500, 54 | message: 'Error while creating app', 55 | }); 56 | } 57 | }, 58 | 59 | retrieveOverallAvg: async (req, res, next) => { 60 | try { 61 | const query = `SELECT EXTRACT(epoch from avg(duration)) * 1000 AS duration_avg_ms FROM spans WHERE parent_id is null AND app_id = $1 AND timestamp AT TIME ZONE 'GMT' AT TIME ZONE $4 BETWEEN ($2 AT TIME ZONE $4) AND ($3 AT TIME ZONE $4);`; 62 | const values = [ 63 | req.params.appId, 64 | res.locals.startDate, 65 | res.locals.endDate, 66 | res.locals.timezone, 67 | ]; 68 | const data = await db.query(query, values); 69 | res.locals.metrics.overallAvg = data.rows[0].duration_avg_ms; 70 | return next(); 71 | } catch (err) { 72 | return next({ 73 | log: `Error in retrieveOverallAvg controller method: ${err}`, 74 | status: 500, 75 | message: 'Error while retrieving data', 76 | }); 77 | } 78 | }, 79 | 80 | retrieveTotalTraces: async (req, res, next) => { 81 | try { 82 | const query = `SELECT CAST (COUNT(DISTINCT trace_id) AS INTEGER) AS trace_count FROM spans WHERE app_id = $1 AND timestamp AT TIME ZONE 'GMT' AT TIME ZONE $4 BETWEEN ($2 AT TIME ZONE $4) AND ($3 AT TIME ZONE $4);`; 83 | const values = [ 84 | req.params.appId, 85 | res.locals.startDate, 86 | res.locals.endDate, 87 | res.locals.timezone, 88 | ]; 89 | const data = await db.query(query, values); 90 | res.locals.metrics.traceCount = data.rows[0].trace_count; 91 | return next(); 92 | } catch (err) { 93 | return next({ 94 | log: `Error in retrieveTotalTraces controller method: ${err}`, 95 | status: 500, 96 | message: 'Error while retrieving data', 97 | }); 98 | } 99 | }, 100 | 101 | retrieveAvgPageDurations: async (req, res, next) => { 102 | try { 103 | const query = `SELECT http_target as page, EXTRACT(epoch from avg(duration)) * 1000 AS ms_avg FROM spans WHERE parent_id is null AND app_id = $1 AND spans.timestamp >= $2::timestamptz AND spans.timestamp <= $3::timestamptz GROUP BY http_target ORDER BY avg(duration) desc LIMIT 5;`; 104 | const values = [ 105 | req.params.appId, 106 | res.locals.startDate, 107 | res.locals.endDate, 108 | ]; 109 | const data = await db.query(query, values); 110 | res.locals.metrics.pageAvgDurations = data.rows; 111 | return next(); 112 | } catch (err) { 113 | return next({ 114 | log: `Error in retrieveTotalTraces controller method: ${err}`, 115 | status: 500, 116 | message: 'Error while retrieving data', 117 | }); 118 | } 119 | }, 120 | 121 | retrieveAvgKindDurations: async (req, res, next) => { 122 | try { 123 | const query = `SELECT kind_id, kind, EXTRACT(epoch from avg(duration)) * 1000 AS ms_avg FROM spans WHERE app_id = $1 AND spans.timestamp >= $2::timestamptz AND spans.timestamp <= $3::timestamptz GROUP BY kind_id, kind;`; 124 | const values = [ 125 | req.params.appId, 126 | res.locals.startDate, 127 | res.locals.endDate, 128 | ]; 129 | const data = await db.query(query, values); 130 | res.locals.metrics.kindAvgDurations = data.rows; 131 | return next(); 132 | } catch (err) { 133 | return next({ 134 | log: `Error in retrieveTotalTraces controller method: ${err}`, 135 | status: 500, 136 | message: 'Error while retrieving data', 137 | }); 138 | } 139 | }, 140 | 141 | retrieveAvgKindDurationsOverTime: async (req, res, next) => { 142 | const query = `SELECT to_char(TIMEZONE($2, periods.datetime), $4) as period, 143 | CASE WHEN EXTRACT(epoch from avg(duration) filter (where kind_id = 0))>0 THEN EXTRACT(epoch from avg(duration) filter (where kind_id = 0)) * 1000 ELSE 0 END AS internal, 144 | CASE WHEN EXTRACT(epoch from avg(duration) filter (where kind_id = 1))>0 THEN EXTRACT(epoch from avg(duration) filter (where kind_id = 1)) * 1000 ELSE 0 END AS server, 145 | CASE WHEN EXTRACT(epoch from avg(duration) filter (where kind_id = 2))>0 THEN EXTRACT(epoch from avg(duration) filter (where kind_id = 2)) * 1000 ELSE 0 END AS client 146 | FROM ( 147 | select generate_series(date_trunc($5, $6::timestamptz), $7, $3) datetime) as periods 148 | left outer join spans on spans.timestamp AT TIME ZONE 'GMT' <@ tstzrange(datetime, datetime + $3::interval) and app_id = $1 149 | GROUP BY periods.datetime 150 | ORDER BY periods.datetime`; 151 | try { 152 | const values = [ 153 | req.params.appId, 154 | res.locals.timezone, 155 | res.locals.intervalBy, 156 | res.locals.format, 157 | res.locals.intervalUnit, 158 | res.locals.startDate, 159 | res.locals.endDate, 160 | ]; 161 | const data = await db.query(query, values); 162 | res.locals.metrics.kindAvgDurationsOverTime = data.rows; 163 | return next(); 164 | } catch (err) { 165 | return next({ 166 | log: `Error in retrieveAvgKindDurationsOverTime controller method: ${err}`, 167 | status: 500, 168 | message: 'Error while retrieving data', 169 | }); 170 | } 171 | }, 172 | 173 | retrievePages: async (req, res, next) => { 174 | try { 175 | const query = 176 | 'SELECT pages._id, spans.http_target as page, pages.app_id as api_id, pages.created_on FROM spans inner join pages on spans.http_target = pages.http_target and spans.app_id = pages.app_id WHERE spans.parent_id is null AND spans.app_id = $1 GROUP BY pages._id, spans.http_target, pages.app_id, pages.created_on ORDER BY avg(duration) desc;'; 177 | const values = [req.params.appId]; 178 | const data = await db.query(query, values); 179 | res.locals.metrics.pages = data.rows; 180 | return next(); 181 | } catch (err) { 182 | return next({ 183 | log: `Error in retrieveTotalTraces controller method: ${err}`, 184 | status: 500, 185 | message: 'Error while retrieving data', 186 | }); 187 | } 188 | }, 189 | 190 | setInterval: (req, res, next) => { 191 | const { start, end } = req.query; 192 | 193 | const startDate = start 194 | ? new Date(start as string) 195 | : new Date(Date.now() - 86400000); // if start is empty, set to yesterday 196 | const endDate = end ? new Date(end as string) : new Date(); // if end is empty, set to now 197 | const intervalSeconds = (endDate.getTime() - startDate.getTime()) / 1000; // get seconds of difference between start and end 198 | if (intervalSeconds < 0) 199 | return next({ 200 | log: `Error in setInterval controller method: End date is before start date`, 201 | status: 400, 202 | message: 'End date is before start date', 203 | }); 204 | 205 | let format = 'FMHH12:MI AM'; // default 206 | let intervalUnit = 'hour'; // default 207 | let intervalBy = '1 hour'; // default 208 | 209 | // TODO: change this to a calculation :( 210 | if (intervalSeconds <= 300) { 211 | // 5 minutes 212 | format = 'FMHH12:MI:SS AM'; 213 | intervalUnit = 'second'; 214 | intervalBy = '10 second'; 215 | } else if (intervalSeconds <= 900) { 216 | // 15 minutes 217 | format = 'FMHH12:MI:SS AM'; 218 | intervalUnit = 'second'; 219 | intervalBy = '30 second'; 220 | } else if (intervalSeconds <= 1800) { 221 | // 30 minutes 222 | format = 'FMHH12:MI AM'; 223 | intervalUnit = 'minute'; 224 | intervalBy = '1 minute'; 225 | } else if (intervalSeconds <= 3600) { 226 | // 1 hour 227 | format = 'FMHH12:MI AM'; 228 | intervalUnit = 'minute'; 229 | intervalBy = '2 minute'; 230 | } else if (intervalSeconds <= 21600) { 231 | // 6 hours 232 | format = 'FMHH12:MI AM'; 233 | intervalUnit = 'minute'; 234 | intervalBy = '15 minute'; 235 | } else if (intervalSeconds <= 43200) { 236 | // 12 hours 237 | format = 'FMHH12:MI AM'; 238 | intervalUnit = 'minute'; 239 | intervalBy = '30 minute'; 240 | } else if (intervalSeconds <= 172800) { 241 | // 2 days 242 | format = 'FMMM/FMDD FMHH12:MI AM'; 243 | intervalUnit = 'hour'; 244 | intervalBy = '1 hour'; 245 | } else if (intervalSeconds <= 518400) { 246 | // 6 days 247 | format = 'FMMM/FMDD FMHH12:MI:SS AM'; 248 | intervalUnit = 'hour'; 249 | intervalBy = '4 hour'; 250 | } else if (intervalSeconds <= 2592000) { 251 | // 30 days 252 | format = 'FMMM/FMDD'; 253 | intervalUnit = 'day'; 254 | intervalBy = '1 day'; 255 | } else if (intervalSeconds <= 5184000) { 256 | // 60 days 257 | format = 'FMMM/FMDD'; 258 | intervalUnit = 'day'; 259 | intervalBy = '2 day'; 260 | } else if (intervalSeconds <= 7776000) { 261 | // 90 days 262 | format = 'FMMM/FMDD'; 263 | intervalUnit = 'day'; 264 | intervalBy = '3 day'; 265 | } else if (intervalSeconds <= 10368000) { 266 | // 120 days 267 | format = 'FMMM/FMDD/YYYY'; 268 | intervalUnit = 'week'; 269 | intervalBy = '1 week'; 270 | } else if (intervalSeconds <= 77760000) { 271 | // 900 days 272 | format = 'FMMM/FMDD/YYYY'; 273 | intervalUnit = 'month'; 274 | intervalBy = '1 month'; 275 | } else if (intervalSeconds > 77760000) { 276 | // > 900 days 277 | format = 'FMMM/FMDD/YYYY'; 278 | intervalUnit = 'year'; 279 | intervalBy = '1 year'; 280 | } 281 | 282 | res.locals.format = format; // format of resulting period 283 | res.locals.intervalUnit = intervalUnit; // what to round times to 284 | res.locals.intervalBy = intervalBy; // what separates each period 285 | res.locals.startDate = startDate; 286 | res.locals.endDate = endDate; 287 | 288 | return next(); 289 | }, 290 | 291 | setTimezone: (req, res, next) => { 292 | const timezone = req.header('User-Timezone'); 293 | 294 | res.locals.timezone = timezone || 'GMT'; 295 | 296 | return next(); 297 | }, 298 | 299 | initializeMetrics: (req, res, next) => { 300 | res.locals.metrics = {}; 301 | 302 | return next(); 303 | }, 304 | }; 305 | 306 | type AppsController = { 307 | createApiKey: RequestHandler; 308 | registerApp: RequestHandler; 309 | retrieveApps: RequestHandler; 310 | retrieveOverallAvg: RequestHandler; 311 | retrieveTotalTraces: RequestHandler; 312 | retrieveAvgPageDurations: RequestHandler; 313 | retrieveAvgKindDurations: RequestHandler; 314 | retrieveAvgKindDurationsOverTime: RequestHandler; 315 | retrievePages: RequestHandler; 316 | setInterval: RequestHandler; 317 | setTimezone: RequestHandler; 318 | initializeMetrics: RequestHandler; 319 | }; 320 | 321 | export default appsController; 322 | -------------------------------------------------------------------------------- /server/controllers/authenticateController.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; // Import JSON Web Token library 2 | import { Request, RequestHandler } from 'express'; 3 | import UserController from './userController'; 4 | const authenticateController: AuthenticateController = { 5 | authenticate: async (req, res, next) => { 6 | let token; 7 | 8 | if (process.env.NODE_ENV === 'development') { 9 | token = jwt.sign({ userId: 1 }, process.env.JWT_SECRET as jwt.Secret, { 10 | expiresIn: '1h', 11 | }); 12 | } else token = req.cookies.jwtToken; 13 | 14 | // If no token is provided, send 400 status and end the function 15 | if (!token) { 16 | return next({ 17 | log: `Error in authenticateController controller method: No token provided`, 18 | status: 400, 19 | message: 'No token provided', 20 | }); 21 | } 22 | 23 | // Verify the provided token with the secret key 24 | try { 25 | const decoded = ( 26 | jwt.verify( 27 | token, 28 | process.env.JWT_SECRET || ('MISSING_SECRET' as jwt.Secret), 29 | ) 30 | ); 31 | 32 | // If token is valid, attach decoded (user) to the request object 33 | req.user = decoded; 34 | 35 | // Pass control to the next middleware function in the stack 36 | return next(); 37 | } catch (e) { 38 | // If an error occurred (indicating invalid token), send 401 status and end the function 39 | if (e instanceof Error) { 40 | return next({ 41 | log: `Error in authenticateController: ${e}`, 42 | status: 401, 43 | message: 'Invalid token provided', 44 | }); 45 | } else throw e; 46 | } 47 | }, 48 | }; 49 | 50 | type AuthenticateController = { 51 | authenticate: RequestHandler; 52 | }; 53 | 54 | export default authenticateController; // Export the function for use in other files 55 | -------------------------------------------------------------------------------- /server/controllers/pagesController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import db from '../models/dataModels'; 3 | 4 | const pagesController: PagesController = { 5 | retrieveOverallAvg: async (req, res, next) => { 6 | try { 7 | const query = `SELECT EXTRACT(epoch from avg(duration)) * 1000 AS duration_avg_ms 8 | FROM spans INNER JOIN pages on spans.http_target = pages.http_target and spans.app_id = spans.app_id 9 | WHERE parent_id is null AND spans.app_id = $1 AND pages._id = $4 AND spans.timestamp >= $2::timestamptz AND spans.timestamp <= $3::timestamptz;`; 10 | const values = [ 11 | req.params.appId, 12 | res.locals.startDate, 13 | res.locals.endDate, 14 | req.params.pageId, 15 | ]; 16 | const data = await db.query(query, values); 17 | res.locals.metrics.overallAvg = data.rows[0].duration_avg_ms; 18 | return next(); 19 | } catch (err) { 20 | return next({ 21 | log: `Error in retrieveOverallAvg controller method: ${err}`, 22 | status: 500, 23 | message: 'Error while retrieving data', 24 | }); 25 | } 26 | }, 27 | 28 | retrieveTotalTraces: async (req, res, next) => { 29 | try { 30 | const query = `SELECT CAST (COUNT(DISTINCT trace_id) AS INTEGER) AS trace_count 31 | FROM spans INNER JOIN pages on spans.http_target = pages.http_target and spans.app_id = spans.app_id 32 | WHERE spans.app_id = $1 AND pages._id = $4 AND spans.timestamp >= $2::timestamptz AND spans.timestamp <= $3::timestamptz;`; 33 | const values = [ 34 | req.params.appId, 35 | res.locals.startDate, 36 | res.locals.endDate, 37 | req.params.pageId, 38 | ]; 39 | const data = await db.query(query, values); 40 | res.locals.metrics.traceCount = data.rows[0].trace_count; 41 | return next(); 42 | } catch (err) { 43 | return next({ 44 | log: `Error in retrieveTotalTraces controller method: ${err}`, 45 | status: 500, 46 | message: 'Error while retrieving data', 47 | }); 48 | } 49 | }, 50 | 51 | retrieveAvgPageDurations: async (req, res, next) => { 52 | const query = `SELECT to_char(TIMEZONE($2, periods.datetime), $4) as period, 53 | CASE WHEN EXTRACT(epoch from avg(duration))>0 THEN EXTRACT(epoch from avg(duration)) * 1000 ELSE 0 END AS "Avg. duration (ms)" 54 | FROM ( 55 | SELECT generate_series(date_trunc($5, $6::timestamptz), $7, $3) datetime) as periods 56 | LEFT OUTER JOIN spans on spans.timestamp AT TIME ZONE 'GMT' <@ tstzrange(datetime, datetime + $3::interval) AND spans.app_id = $1 57 | AND spans.http_target IN (SELECT http_target FROM pages WHERE app_id = $1 AND _id = $8) 58 | GROUP BY periods.datetime 59 | ORDER BY periods.datetime`; 60 | try { 61 | const values = [ 62 | req.params.appId, 63 | res.locals.timezone, 64 | res.locals.intervalBy, 65 | res.locals.format, 66 | res.locals.intervalUnit, 67 | res.locals.startDate, 68 | res.locals.endDate, 69 | req.params.pageId, 70 | ]; 71 | const data = await db.query(query, values); 72 | res.locals.metrics.avgPageDurationsOverTime = data.rows; 73 | return next(); 74 | } catch (err) { 75 | return next({ 76 | log: `Error in retrieveAvgKindDurationsOverTime controller method: ${err}`, 77 | status: 500, 78 | message: 'Error while retrieving data', 79 | }); 80 | } 81 | }, 82 | 83 | retrieveAvgActionDurations: async (req, res, next) => { 84 | const queryGetActions = `SELECT DISTINCT pages._id, pages.http_target, spans_child.name 85 | FROM spans as spans_child inner join spans as spans_parent on spans_child.trace_id = spans_parent.trace_id 86 | AND spans_parent.app_id = spans_child.app_id 87 | AND spans_parent.parent_id is null 88 | inner join pages on spans_parent.http_target = pages.http_target and spans_parent.app_id = pages.app_id 89 | WHERE spans_child.parent_id is not null AND spans_child.app_id = $1 AND pages._id = $2 90 | AND spans_child.timestamp >= $3::timestamptz AND spans_child.timestamp <= $4::timestamptz`; 91 | 92 | const query = `SELECT to_char(TIMEZONE($2, periods.datetime), $4) as period, 93 | CASE WHEN EXTRACT(epoch from avg(duration))>0 THEN EXTRACT(epoch from avg(duration)) * 1000 ELSE 0 END AS ACTION 94 | FROM ( 95 | select generate_series(date_trunc($5, $6::timestamptz), $7, $3) datetime) as periods 96 | left outer join spans on spans.timestamp AT TIME ZONE 'GMT' <@ tstzrange(datetime, datetime + $3::interval) and spans.app_id = $1 AND spans.name = $8 97 | AND spans.trace_id in (SELECT DISTINCT trace_id FROM spans WHERE http_target = $9) 98 | GROUP BY periods.datetime 99 | ORDER BY periods.datetime`; 100 | 101 | try { 102 | // get array of actions (children requests) 103 | const valuesGetActions = [ 104 | req.params.appId, 105 | req.params.pageId, 106 | res.locals.startDate, 107 | res.locals.endDate, 108 | ]; 109 | 110 | const actionData = await db.query(queryGetActions, valuesGetActions); 111 | if (actionData.rows.length === 0) actionData.rows[0] = {}; 112 | const aggregatedQueryResults: Period[] = []; 113 | // for each action, get aggregation of data based on time period 114 | // uses map and promise.all to run queries in parallel 115 | await Promise.all( 116 | actionData.rows.map(async (el) => { 117 | const values = [ 118 | req.params.appId, 119 | res.locals.timezone, 120 | res.locals.intervalBy, 121 | res.locals.format, 122 | res.locals.intervalUnit, 123 | res.locals.startDate, 124 | res.locals.endDate, 125 | el.name, 126 | el.http_target, 127 | ]; 128 | const data = await db.query(query, values); 129 | 130 | // combine data into our aggregated array 131 | data.rows.forEach((row, i) => { 132 | if (aggregatedQueryResults[i] === undefined) { 133 | if (el.http_target) { 134 | aggregatedQueryResults.push({ 135 | period: row.period, 136 | // add key with name of action 137 | [el.name]: row.action, 138 | }); 139 | } else { 140 | aggregatedQueryResults.push({ 141 | period: row.period, 142 | }); 143 | } 144 | } else aggregatedQueryResults[i][el.name] = row.action; 145 | }); 146 | }), 147 | ); 148 | 149 | res.locals.metrics.avgActionDurationsOverTime = aggregatedQueryResults; 150 | return next(); 151 | } catch (err) { 152 | return next({ 153 | log: `Error in retrieveAvgKindDurationsOverTime controller method: ${err}`, 154 | status: 500, 155 | message: 'Error while retrieving data', 156 | }); 157 | } 158 | }, 159 | 160 | retrieveAvgActionData: async (req, res, next) => { 161 | try { 162 | const query = ` 163 | SELECT name as "Name", EXTRACT(epoch from avg(duration)) * 1000 AS "Avg. duration (ms)", count(id) as "Total no. of executions", count(distinct trace_id) as "Total no. of traces", kind as "Kind" 164 | FROM spans 165 | WHERE spans.app_id = $1 AND spans.timestamp >= $2::timestamptz AND spans.timestamp <= $3::timestamptz 166 | AND trace_id IN (SELECT trace_id FROM spans INNER JOIN pages on spans.http_target = pages.http_target WHERE pages._id = $4) 167 | GROUP BY name, kind`; 168 | const values = [ 169 | req.params.appId, 170 | res.locals.startDate, 171 | res.locals.endDate, 172 | req.params.pageId, 173 | ]; 174 | const data = await db.query(query, values); 175 | res.locals.metrics.overallPageData = data.rows; 176 | return next(); 177 | } catch (err) { 178 | return next({ 179 | log: `Error in retrieveOverallAvg controller method: ${err}`, 180 | status: 500, 181 | message: 'Error while retrieving data', 182 | }); 183 | } 184 | }, 185 | }; 186 | 187 | type Period = { 188 | [action: string]: number | string; 189 | }; 190 | 191 | type PagesController = { 192 | retrieveOverallAvg: RequestHandler; 193 | retrieveTotalTraces: RequestHandler; 194 | retrieveAvgPageDurations: RequestHandler; 195 | retrieveAvgActionDurations: RequestHandler; 196 | retrieveAvgActionData: RequestHandler; 197 | }; 198 | 199 | export default pagesController; 200 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import jwt from 'jsonwebtoken'; 3 | import { RequestHandler } from 'express'; 4 | import db from '../models/dataModels'; 5 | import getUser from '../../helpers/getUser'; 6 | 7 | const userController: UserController = { 8 | registerUser: async (req, res, next) => { 9 | try { 10 | const { username, password } = req.body; 11 | 12 | // Validate unique username 13 | const user = await getUser(username); 14 | 15 | // If user is found in DB (username taken), throw an error 16 | if (user.rows.length) { 17 | throw new Error('Username is unavailable'); 18 | } 19 | 20 | const hashedPassword = await bcrypt.hash(password, 10); 21 | const query = 22 | 'INSERT INTO users(username, password) VALUES($1, $2) RETURNING *'; 23 | const values = [username, hashedPassword]; 24 | const newUser = await db.query(query, values); 25 | res.locals.user = newUser.rows[0]; 26 | 27 | return next(); 28 | } catch (err) { 29 | // If an error occurs, pass it to the error handling middleware 30 | return next({ 31 | log: `Error in registerUser controller method: ${err}`, 32 | status: 400, 33 | message: 'Error while registering new user', 34 | }); 35 | } 36 | }, 37 | 38 | loginUser: async (req, res, next) => { 39 | try { 40 | const { username, password } = req.body; 41 | 42 | // Get user with the given username 43 | const user = await getUser(username); 44 | 45 | // If no user is found with this username, throw an error 46 | if (!user.rows.length) { 47 | throw new Error('Incorrect password or username'); 48 | } 49 | 50 | // Check if the password is correct. bcrypt.compare will hash the provided password and compare it to the stored hash. 51 | const match = await bcrypt.compare(password, user.rows[0].password); 52 | 53 | // If the passwords do not match, throw an error 54 | if (!match) { 55 | throw new Error('Incorrect password or username'); 56 | } 57 | 58 | // Create a JWT. The payload is the user's id, the secret key is stored in env, and it will expire in 1 hour 59 | const token = jwt.sign( 60 | { userId: user.rows[0]._id }, 61 | process.env.JWT_SECRET as jwt.Secret, 62 | { expiresIn: '30d' }, 63 | ); 64 | 65 | // Set the JWT token as an HTTP-only cookie 66 | res.cookie('jwtToken', token, { httpOnly: true }); 67 | 68 | // Save the token and the username to res.locals for further middleware to use 69 | res.locals.user = { 70 | token, 71 | user: user.rows[0].username, 72 | }; 73 | 74 | return next(); 75 | } catch (err) { 76 | if (err instanceof Error) { 77 | // If an error occurs, pass it to the error handling middleware 78 | return next({ 79 | log: `Error in loginUser controller method ${err}`, 80 | status: 400, 81 | message: { err: err.message }, 82 | }); 83 | } else throw err; 84 | } 85 | }, 86 | 87 | logoutUser: async (req, res, next) => { 88 | res.clearCookie('jwtToken'); 89 | return next(); 90 | }, 91 | 92 | userInfo: async (req, res, next) => { 93 | try { 94 | const query = 95 | 'SELECT users._id, users.username, created_on FROM users WHERE _id = $1'; 96 | const values = [req.user.userId]; 97 | const data = await db.query(query, values); 98 | res.locals.user = data.rows[0]; 99 | return next(); 100 | } catch (err) { 101 | return next({ 102 | log: `Error in retrieveTotalTraces controller method: ${err}`, 103 | status: 500, 104 | message: 'Error while retrieving data', 105 | }); 106 | } 107 | }, 108 | }; 109 | 110 | type UserController = { 111 | registerUser: RequestHandler; 112 | loginUser: RequestHandler; 113 | logoutUser: RequestHandler; 114 | userInfo: RequestHandler; 115 | }; 116 | 117 | export default userController; 118 | -------------------------------------------------------------------------------- /server/models/dataModels.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from 'pg'; 2 | import 'dotenv/config'; 3 | 4 | const PG_URI = process.env.PG_URI || undefined; 5 | 6 | // create a new pool here using the connection string above 7 | const pool = new Pool({ 8 | connectionString: PG_URI, 9 | }); 10 | 11 | // We export an object that contains a property called query, 12 | // which is a function that returns the invocation of pool.query() after logging the query 13 | // This will be required in the controllers to be the access point to the database 14 | export default { 15 | query: (text: QueryParams[0], params: QueryParams[1]) => { 16 | return pool.query(text, params); 17 | }, 18 | }; 19 | 20 | type QueryParams = Parameters; 21 | -------------------------------------------------------------------------------- /server/routes/apiRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const apiRouter = express.Router(); 4 | // TODO also login user during registration 5 | // TODO finish sending responses 6 | apiRouter.post('/', (req, res, next) => { 7 | res.status(201).send('/ api controller not yet implemented'); 8 | }); 9 | 10 | export default apiRouter; 11 | -------------------------------------------------------------------------------- /server/routes/appsRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import pagesRouter from './pagesRouter'; 3 | import appsController from '../controllers/appsController'; 4 | 5 | const appsRouter = express.Router(); 6 | 7 | appsRouter.use('/:appId/pages', pagesRouter); 8 | 9 | appsRouter.get( 10 | '/:appId/data', 11 | appsController.initializeMetrics, 12 | appsController.setInterval, 13 | appsController.setTimezone, 14 | appsController.retrievePages, 15 | appsController.retrieveOverallAvg, 16 | appsController.retrieveTotalTraces, 17 | appsController.retrieveAvgPageDurations, 18 | appsController.retrieveAvgKindDurations, 19 | appsController.retrieveAvgKindDurationsOverTime, 20 | (req, res, next) => { 21 | res.status(200).send(res.locals.metrics); 22 | }, 23 | ); 24 | 25 | appsRouter.delete('/:appId', (req, res, next) => { 26 | res.status(204).send('app deletion controller not yet implemented'); 27 | }); 28 | 29 | appsRouter.post( 30 | '/', 31 | appsController.createApiKey, 32 | appsController.registerApp, 33 | (req, res, next) => { 34 | res.status(201).send(res.locals.app); 35 | }, 36 | ); 37 | 38 | appsRouter.get('/', appsController.retrieveApps, (req, res, next) => { 39 | res.status(200).send(res.locals.apps); 40 | }); 41 | 42 | export default appsRouter; 43 | -------------------------------------------------------------------------------- /server/routes/pagesRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import appsController from '../controllers/appsController'; 3 | import pagesController from '../controllers/pagesController'; 4 | // have to use mergeParams to get appId from parent router 5 | const pagesRouter = express.Router({ mergeParams: true }); 6 | 7 | pagesRouter.get( 8 | '/:pageId/data', 9 | appsController.initializeMetrics, 10 | appsController.setInterval, 11 | appsController.setTimezone, 12 | pagesController.retrieveOverallAvg, 13 | pagesController.retrieveTotalTraces, 14 | pagesController.retrieveAvgPageDurations, 15 | pagesController.retrieveAvgActionDurations, 16 | pagesController.retrieveAvgActionData, 17 | (req, res, next) => { 18 | res.status(200).send(res.locals.metrics); 19 | }, 20 | ); 21 | 22 | pagesRouter.get( 23 | '/', 24 | appsController.initializeMetrics, 25 | appsController.retrievePages, 26 | (req, res, next) => { 27 | res.status(200).send(res.locals.metrics.pages); 28 | }, 29 | ); 30 | 31 | export default pagesRouter; 32 | -------------------------------------------------------------------------------- /server/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import authenticateController from '../controllers/authenticateController'; 3 | import userController from '../controllers/userController'; 4 | 5 | const userRouter = express.Router(); 6 | 7 | userRouter.post( 8 | '/register', 9 | userController.registerUser, 10 | userController.loginUser, 11 | (req, res, next) => { 12 | res.status(201).json(res.locals.user); 13 | }, 14 | ); 15 | 16 | userRouter.post('/login', userController.loginUser, (req, res, next) => { 17 | res.status(200).json(res.locals.user); 18 | }); 19 | 20 | userRouter.get( 21 | '/authenticate', 22 | authenticateController.authenticate, 23 | userController.userInfo, 24 | (req, res, next) => { 25 | res.status(200).send(res.locals.user); 26 | }, 27 | ); 28 | 29 | userRouter.delete( 30 | '/logout', 31 | authenticateController.authenticate, 32 | userController.logoutUser, 33 | (req, res, next) => { 34 | res.sendStatus(204); 35 | }, 36 | ); 37 | 38 | export default userRouter; 39 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | Express, 3 | Request, 4 | Response, 5 | NextFunction, 6 | ErrorRequestHandler, 7 | } from 'express'; 8 | import path from 'path'; 9 | import userRouter from './routes/userRouter'; 10 | import appsRouter from './routes/appsRouter'; 11 | import apiRouter from './routes/apiRouter'; 12 | import authenticateController from './controllers/authenticateController'; 13 | import cookieParser from 'cookie-parser'; 14 | 15 | const PORT = process.env.PORT || 3000; 16 | 17 | const app: Express = express(); 18 | 19 | /** 20 | * Automatically parse urlencoded body content and form data from incoming requests and place it 21 | * in req.body 22 | */ 23 | app.use(express.json()); 24 | app.use(express.urlencoded({ extended: true })); 25 | app.use(cookieParser()); 26 | 27 | /** 28 | * --- Express Routes --- 29 | * Express will attempt to match these routes in the order they are declared here. 30 | * If a route handler / middleware handles a request and sends a response without 31 | * calling `next()`, then none of the route handlers after that route will run! 32 | * This can be very useful for adding authorization to certain routes... 33 | */ 34 | 35 | app.use('/user', userRouter); 36 | app.use('/apps', authenticateController.authenticate, appsRouter); 37 | app.use('/api', apiRouter); 38 | 39 | app.get('/testerror', (req, res, next) => { 40 | next({ 41 | log: `getDBName has an error`, 42 | status: 400, 43 | message: { err: 'An error occurred' }, 44 | }); 45 | }); 46 | 47 | app.get('/test', (req, res) => { 48 | res.status(200).send('Hello world'); 49 | }); 50 | 51 | // if running from production, serve bundled files 52 | if (process.env.NODE_ENV === 'production') { 53 | app.use(express.static(path.join(path.resolve(), 'dist'))); 54 | app.get('/*', function (req, res) { 55 | res.sendFile(path.join(path.resolve(), 'dist', 'index.html')); 56 | }); 57 | } 58 | 59 | /** 60 | * 404 handler 61 | */ 62 | app.use('*', (req, res) => { 63 | res.status(404).send('Not Found'); 64 | }); 65 | 66 | /** 67 | * Global error handler 68 | */ 69 | app.use( 70 | ( 71 | err: ErrorRequestHandler, 72 | req: Request, 73 | res: Response, 74 | next: NextFunction, 75 | ) => { 76 | const defaultErr = { 77 | log: 'Express error handler caught unknown middleware error', 78 | status: 500, 79 | message: { err: 'An error occurred' }, 80 | }; 81 | const errorObj = Object.assign({}, defaultErr, err); 82 | console.log(errorObj.log); 83 | return res.status(errorObj.status).json(errorObj.message); 84 | }, 85 | ); 86 | 87 | export default app.listen(PORT, () => console.log('listening on port ', PORT)); 88 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { useState } from 'react'; 3 | import Home from './pages/Home'; 4 | import DashboardPage from './pages/Dashboard'; 5 | import NotFound from './pages/NotFound/NotFound'; 6 | import { UserContext } from './contexts/userContexts'; 7 | import { APIContext } from './contexts/dashboardContexts'; 8 | import ProtectedRoute from './components/ProtectedRoute'; 9 | 10 | function App() { 11 | const [username, setUsername] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | const [loggedIn, setLoggedIn] = useState(false); 14 | const [apiKey, setApiKey] = useState(null); 15 | 16 | return ( 17 | <> 18 | 19 | 29 | 30 | } /> 31 | 35 | 36 | 37 | } 38 | /> 39 | } /> 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /src/assets/GitHub_Logo_White.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/GitHub_Logo_White.webp -------------------------------------------------------------------------------- /src/assets/NV-logo-transparent.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/NV-logo-transparent.webp -------------------------------------------------------------------------------- /src/assets/NV-logo-white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/NV-logo-white.webp -------------------------------------------------------------------------------- /src/assets/NextView-logo-pink-transparent.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/NextView-logo-pink-transparent.webp -------------------------------------------------------------------------------- /src/assets/NextView-logo-white-48x48.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/NextView-logo-white-48x48.webp -------------------------------------------------------------------------------- /src/assets/Party_Popper_Emojipedia.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/Party_Popper_Emojipedia.webp -------------------------------------------------------------------------------- /src/assets/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/checkmark.png -------------------------------------------------------------------------------- /src/assets/copy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/copy.webp -------------------------------------------------------------------------------- /src/assets/eduardo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/eduardo.webp -------------------------------------------------------------------------------- /src/assets/evram.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/evram.webp -------------------------------------------------------------------------------- /src/assets/github-mark-white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/github-mark-white.webp -------------------------------------------------------------------------------- /src/assets/github-mark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/github-mark.webp -------------------------------------------------------------------------------- /src/assets/hands.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/hands.webp -------------------------------------------------------------------------------- /src/assets/kinski.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/kinski.webp -------------------------------------------------------------------------------- /src/assets/linked-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/linked-in.png -------------------------------------------------------------------------------- /src/assets/npm-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/npm-black.png -------------------------------------------------------------------------------- /src/assets/npm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/npm.webp -------------------------------------------------------------------------------- /src/assets/overview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/overview.webp -------------------------------------------------------------------------------- /src/assets/scott.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/scott.webp -------------------------------------------------------------------------------- /src/assets/sooji.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/sooji.webp -------------------------------------------------------------------------------- /src/assets/star.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/star.webp -------------------------------------------------------------------------------- /src/assets/team.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/team.webp -------------------------------------------------------------------------------- /src/assets/telescope.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/NextView/bc102f5c1b489fa169f39601064a354af8bc55c2/src/assets/telescope.webp -------------------------------------------------------------------------------- /src/components/Box.tsx: -------------------------------------------------------------------------------- 1 | interface BoxProps { 2 | title: string; 3 | data: number; 4 | } 5 | 6 | const Box = ({ title, data }: BoxProps) => { 7 | return ( 8 |
9 | {title} 10 | {data && 11 | (title === 'Average Page Load Duration' || 12 | title === 'Average Page Load Duration') ? ( 13 | {data}ms 14 | ) : ( 15 | {data} 16 | )} 17 |
18 | ); 19 | }; 20 | 21 | export default Box; 22 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | // interface to declare all our prop types 2 | interface Props { 3 | children?: React.ReactNode; 4 | onClick?: () => void; 5 | variant?: string; // default, primary, info 6 | size?: string; // sm, md, lg 7 | disabled?: boolean; 8 | className?: string; 9 | type?: 'button' | 'submit' | 'reset' | undefined; 10 | } 11 | 12 | // button component, consuming props 13 | const Button: React.FC = ({ 14 | className, 15 | children, 16 | onClick, 17 | variant = 'default', 18 | size = 'md', 19 | disabled, 20 | type, 21 | ...rest 22 | }) => { 23 | return ( 24 | 37 | ); 38 | }; 39 | 40 | export default Button; 41 | -------------------------------------------------------------------------------- /src/components/CopyInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | interface Props { 3 | text: string; 4 | children?: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export const CopyInput: React.FC = ({ text, children, className }) => { 9 | const [copyClicked, setCopyClicked] = useState(false); 10 | return ( 11 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Feature.tsx: -------------------------------------------------------------------------------- 1 | // interface to declare all our prop types 2 | interface Props { 3 | children: React.ReactNode; 4 | className?: string; 5 | } 6 | 7 | // button component, consuming props 8 | const Feature: React.FC = ({ className, children, ...rest }) => { 9 | return ( 10 |
17 | {children} 18 |
19 | ); 20 | }; 21 | 22 | export default Feature; 23 | -------------------------------------------------------------------------------- /src/components/NPMCopyInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const NPMCopyInput = () => { 4 | const [copyClicked, setCopyClicked] = useState(false); 5 | return ( 6 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | 3 | const ProtectedRoute: React.FC = ({ children }) => { 4 | const storedUser = JSON.parse(localStorage.getItem('user')); 5 | 6 | return storedUser ? children : ; 7 | }; 8 | 9 | export default ProtectedRoute; 10 | -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | const Spinner = () => { 2 | return ( 3 |
4 |
8 |
12 |

Loading...

13 |
14 | ); 15 | }; 16 | 17 | export default Spinner; 18 | -------------------------------------------------------------------------------- /src/contexts/dashboardContexts.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface Period { 4 | interval: number; 5 | unit: string; 6 | } 7 | 8 | interface TextBox { 9 | overallAvg: number; 10 | traceCount: number; 11 | } 12 | 13 | export interface LineDataItem { 14 | period: string; 15 | client: number; 16 | server: number; 17 | internal: number; 18 | } 19 | 20 | export interface BarDataItem { 21 | page: string; 22 | ms_avg: number; 23 | } 24 | 25 | export interface PieDataItem { 26 | kind_id: number; 27 | kind: string; 28 | ms_avg: number; 29 | } 30 | 31 | // Context Types 32 | 33 | export interface StartContextType { 34 | start: string; 35 | setStart: (value: string) => void; 36 | } 37 | 38 | export interface EndContextType { 39 | end: string; 40 | setEnd: (value: string) => void; 41 | } 42 | 43 | export interface PeriodContextType { 44 | start: string; 45 | setStart: (value: string) => void; 46 | end: string; 47 | setEnd: (value: string) => void; 48 | } 49 | 50 | export interface TraceTextboxContextType { 51 | traceCount: number; 52 | setTraceCount: (value: number) => void; 53 | } 54 | 55 | export interface DurationTextboxContextType { 56 | overallAvgDuration: number; 57 | setOverallAvgDuration: number; 58 | } 59 | 60 | export interface BarGraphContextType { 61 | barData: BarDataItem[] | null; 62 | setBarData: (value: BarDataItem[]) => void; 63 | } 64 | 65 | export interface OVLineChartContextType { 66 | lineData: LineDataItem[] | null; 67 | setLineData: (value: LineDataItem[]) => void; 68 | } 69 | 70 | export interface PieChartContextType { 71 | pieData: PieDataItem[] | null; 72 | setPieData: (value: PieDataItem[]) => void; 73 | } 74 | 75 | // export const PeriodContext = createContext(null); 76 | export const PeriodContext = createContext(null); 77 | export const TraceTextboxContext = createContext(0); 78 | export const DurationTextboxContext = createContext(0); 79 | export const TextboxContext = createContext(null); 80 | export const BarGraphContext = createContext( 81 | undefined, 82 | ); 83 | export const OVLineChartContext = createContext< 84 | OVLineChartContextType | undefined 85 | >(undefined); 86 | export const PieChartContext = createContext( 87 | undefined, 88 | ); 89 | export const StartContext = createContext(null); 90 | export const EndContext = createContext(null); 91 | 92 | export const OverviewDataContext = createContext(null); 93 | 94 | export const APIContext = createContext(null); 95 | 96 | export const PageContext = createContext(null); 97 | -------------------------------------------------------------------------------- /src/contexts/userContexts.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface UserContextType { 4 | username: string; 5 | setUsername: (value: string) => void; 6 | password: string; 7 | setPassword: (value: string) => void; 8 | loggedIn: boolean; 9 | setLoggedIn: (value: boolean) => void; 10 | } 11 | 12 | export const UserContext = createContext(null); 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600'); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | :root { 7 | --primary-color: #e2f0f9; 8 | --secondary-color: #286fb4; 9 | --accent-color: #df4c73; 10 | 11 | font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; 12 | line-height: 1.5; 13 | font-weight: 400; 14 | 15 | color-scheme: light dark; 16 | 17 | color: #213547; 18 | background-color: #ffffff; 19 | 20 | font-synthesis: none; 21 | text-rendering: optimizeLegibility; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-text-size-adjust: 100%; 25 | } 26 | 27 | ::-webkit-scrollbar { 28 | height: 12px; 29 | width: 12px; 30 | overflow: visible; 31 | } 32 | 33 | ::-webkit-scrollbar-track { 34 | background-color: #ededed; 35 | background-clip: padding-box; 36 | border: solid rgba(0, 0, 0, 0); 37 | border-width: 3px; 38 | border-radius: 100px; 39 | } 40 | 41 | ::-webkit-scrollbar-corner { 42 | background: rgba(0, 0, 0, 0); 43 | } 44 | 45 | ::-webkit-scrollbar-thumb { 46 | background-color: #a6a6a6; 47 | border-radius: 100px; 48 | background-clip: padding-box; 49 | border: solid rgba(0, 0, 0, 0); 50 | border-width: 3px; 51 | } 52 | 53 | ::-webkit-scrollbar-button { 54 | height: 0; 55 | width: 0; 56 | } 57 | 58 | body { 59 | margin: 0px; 60 | width: 100vw; 61 | height: 100vh; 62 | overflow: hidden; 63 | position: absolute; 64 | padding: 0px; 65 | } 66 | 67 | h1 { 68 | font-size: 3.2em; 69 | line-height: 1.1; 70 | } 71 | 72 | img { 73 | object-fit: cover; 74 | } 75 | 76 | li { 77 | margin: 10px; 78 | } 79 | 80 | @layer components { 81 | .sideNavBar-icon { 82 | @apply relative mx-auto mb-2 mt-2 flex h-12 w-12 cursor-pointer items-center justify-center rounded-md bg-transparent transition duration-75 ease-linear hover:scale-105; 83 | } 84 | 85 | .btn { 86 | @apply m-2 rounded px-4 py-2 text-xs; 87 | } 88 | .sm { 89 | @apply px-2; 90 | } 91 | 92 | .md { 93 | @apply px-4; 94 | } 95 | 96 | .lg { 97 | @apply px-8 text-base; 98 | } 99 | 100 | .xl { 101 | @apply px-16; 102 | } 103 | 104 | .default { 105 | @apply border border-gray-300 bg-gray-100 text-gray-900; 106 | } 107 | .default:hover { 108 | @apply border-gray-500; 109 | } 110 | 111 | .primary { 112 | @apply bg-primary text-gray-800; 113 | } 114 | .primary:hover { 115 | @apply brightness-95; 116 | } 117 | 118 | .secondary { 119 | @apply bg-secondary font-semibold text-white; 120 | } 121 | .secondary:hover { 122 | @apply brightness-90; 123 | } 124 | 125 | .disable-blur { 126 | image-rendering: crisp-edges; 127 | } 128 | } 129 | 130 | .type::after { 131 | content: '_ '; 132 | animation: cursor 1.1s infinite step-start; 133 | } 134 | 135 | @keyframes cursor { 136 | 50% { 137 | opacity: 0; 138 | } 139 | } 140 | 141 | .textData { 142 | margin-top: 5px; 143 | display: block; 144 | color: rgba(0, 0, 0, 0.56); 145 | } 146 | 147 | .loader { 148 | border-top-color: #3498db; 149 | -webkit-animation: spinner 1.5s linear infinite; 150 | animation: spinner 1.5s linear infinite; 151 | } 152 | 153 | @-webkit-keyframes spinner { 154 | 0% { 155 | -webkit-transform: rotate(0deg); 156 | } 157 | 100% { 158 | -webkit-transform: rotate(360deg); 159 | } 160 | } 161 | 162 | @keyframes spinner { 163 | 0% { 164 | transform: rotate(0deg); 165 | } 166 | 100% { 167 | transform: rotate(360deg); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App.tsx'; 5 | 6 | import './index.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useContext } from 'react'; 2 | import MainDisplay from './MainDisplay/MainDisplay'; 3 | import Sidebar from './Sidebar/Sidebar'; 4 | import Loading from './Loading'; 5 | import { APIContext, PageContext } from '../../contexts/dashboardContexts'; 6 | import dayjs from 'dayjs'; 7 | 8 | const Dashboard = () => { 9 | // values used in fetch requests, setters used in topbar 10 | // initialized to the last 24 hrs 11 | const [start, setStart] = useState(dayjs().subtract(1, 'day').toISOString()); 12 | const [end, setEnd] = useState(dayjs().toISOString()); 13 | 14 | // set in dashboard 15 | const [overviewData, setOverviewData] = useState(null); 16 | 17 | // set in pageDisplay 18 | const [pageData, setPageData] = useState(null); 19 | 20 | // initialized to null in context, set to user key by fetchAppsList() 21 | const { apiKey, setApiKey } = useContext(APIContext); 22 | 23 | // currently set by sidebar button, accessed by context 24 | const [page, setPage] = useState(); 25 | 26 | // fetch apps list and api key 27 | // will not run after api key is set 28 | useEffect(() => { 29 | const fetchAppsList = async () => { 30 | try { 31 | const response = await fetch('/apps'); 32 | const data = await response.json(); 33 | setApiKey(data[0]['id']); 34 | } catch (error: unknown) { 35 | console.log('Data fetching failed', error); 36 | } 37 | }; 38 | if (!apiKey) fetchAppsList(); 39 | }); 40 | 41 | // fetch overview data 42 | // only if user api key is set 43 | useEffect(() => { 44 | const fetchOverviewData = async () => { 45 | try { 46 | const response = await fetch( 47 | `/apps/${apiKey}/data?start=${start}&end=${end}`, 48 | { 49 | headers: { 50 | 'User-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, 51 | }, 52 | }, 53 | ); 54 | const data = await response.json(); 55 | setOverviewData(data); 56 | setPage(); 57 | } catch (error: unknown) { 58 | console.log('Data fetching failed', error); 59 | } 60 | }; 61 | if (apiKey) fetchOverviewData(); 62 | }, [start, end, apiKey]); 63 | 64 | return ( 65 | <> 66 | {overviewData ? ( 67 | 70 |
71 | 72 | 78 |
79 |
80 | ) : ( 81 |
82 | 83 |
84 | )} 85 | 86 | ); 87 | }; 88 | 89 | export default Dashboard; 90 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = () => { 2 | return ( 3 |
4 |
5 |

6 | Loading... 7 |

8 |

9 | This may take a few seconds, please don't close this page. 10 |

11 |
12 | ); 13 | }; 14 | 15 | export default Loading; 16 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/MainDisplay.tsx: -------------------------------------------------------------------------------- 1 | import Topbar from './Topbar'; 2 | import OverviewDisplay from './OverviewDisplay/OverviewDisplay'; 3 | import PageDisplay from './PageDisplay/PageDisplay'; 4 | import { Routes, Route } from 'react-router-dom'; 5 | import NotFound from '../../NotFound/NotFound'; 6 | 7 | const MainDisplay = ({ overviewData, pageData, setStart, setEnd }) => { 8 | return ( 9 |
10 | 11 | 12 | } 15 | /> 16 | } 19 | /> 20 | } /> 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default MainDisplay; 27 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/OverviewDisplay/BarGraph.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart, 3 | Bar, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | Legend, 9 | ResponsiveContainer, 10 | } from 'recharts'; 11 | 12 | const BarGraph = ({ data }) => { 13 | const num = data.length; 14 | 15 | return ( 16 | <> 17 |

Top {num} Slowest Pages

18 | 19 | 28 | 29 | 36 | 45 | val + 'ms'} 49 | /> 50 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default BarGraph; 65 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/OverviewDisplay/HorizontalBarGraph.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart, 3 | Bar, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | Cell, 9 | ResponsiveContainer, 10 | } from 'recharts'; 11 | 12 | const barColors = ['#003f5c', '#bc5090', '#6996e4']; 13 | 14 | const HorizontalBarGragh = ({ data }) => { 15 | return ( 16 | <> 17 |

Average Span Duration

18 | 19 | 29 | 30 | 41 | 54 | val + 'ms'} 58 | /> 59 | 66 | {data && 67 | data.map((data, index) => ( 68 | 69 | ))} 70 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default HorizontalBarGragh; 78 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/OverviewDisplay/LineChart.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LineChart, 3 | Line, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | Legend, 9 | ResponsiveContainer, 10 | } from 'recharts'; 11 | 12 | const LineChartComponent = ({ data }) => { 13 | return ( 14 | <> 15 |

16 | Average Span Load Duration Over Time 17 |

18 | 19 | 28 | 29 | 30 | 31 | val + 'ms'} /> 32 | 33 | 42 | 50 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default LineChartComponent; 65 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/OverviewDisplay/OverviewDisplay.tsx: -------------------------------------------------------------------------------- 1 | import Textbox from './Texbox'; 2 | import HorizontalBarGraph from './HorizontalBarGraph'; 3 | import BarGraph from './BarGraph'; 4 | import LineChart from './LineChart'; 5 | import { useContext, useEffect } from 'react'; 6 | import { PageContext } from '../../../../contexts/dashboardContexts'; 7 | 8 | const OverviewDisplay = ({ overviewData }) => { 9 | const { setPage } = useContext(PageContext); 10 | useEffect(() => setPage(), [setPage]); 11 | 12 | return ( 13 |
14 |
15 |
16 | 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default OverviewDisplay; 36 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/OverviewDisplay/Texbox.tsx: -------------------------------------------------------------------------------- 1 | import Box from '../../../../components/Box'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | const Textbox = ({ traceCount, overallAvg }) => { 5 | const boxData = [ 6 | { title: 'Average Page Load Duration', data: overallAvg }, 7 | { title: 'Total No. of Traces', data: traceCount }, 8 | ]; 9 | return ( 10 |
11 | {boxData.map(({ title, data }) => ( 12 | 13 | ))} 14 |
15 | ); 16 | }; 17 | 18 | export default Textbox; 19 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/Box.tsx: -------------------------------------------------------------------------------- 1 | interface BoxProps { 2 | title: string; 3 | data: number; 4 | } 5 | 6 | const Box = ({ title, data }: BoxProps) => { 7 | return ( 8 |
9 | {title} 10 | {data} 11 |
12 | ); 13 | }; 14 | 15 | export default Box; 16 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/PageDisplay.tsx: -------------------------------------------------------------------------------- 1 | import Textbox from './Textbox'; 2 | import Table from './Table'; 3 | import PageLineChart from './PageLineChart'; 4 | import SpanLineChart from './SpanLineChart'; 5 | import { useParams } from 'react-router-dom'; 6 | import { useContext, useEffect, useState } from 'react'; 7 | import { PageContext } from '../../../../contexts/dashboardContexts'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | import Spinner from '../../../../components/Spinner'; 10 | 11 | const PageDisplay = ({ overviewData }) => { 12 | const { id } = useParams(); 13 | const { pageData, setPageData, apiKey, start, end, page, setPage } = 14 | useContext(PageContext); 15 | 16 | const [loading, setLoading] = useState(true); 17 | useEffect(() => { 18 | setLoading(true); 19 | 20 | const pageId = id; 21 | 22 | for (let i = 0; i < overviewData.pages.length; i++) { 23 | if (overviewData.pages[i]._id == pageId) { 24 | setPage(overviewData.pages[i]); 25 | break; 26 | } 27 | } 28 | 29 | const fetchPageData = async () => { 30 | try { 31 | const response = await fetch( 32 | `/apps/${apiKey}/pages/${pageId}/data?start=${start}&end=${end}`, 33 | { 34 | headers: { 35 | 'User-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, 36 | }, 37 | }, 38 | ); 39 | const data = await response.json(); 40 | setPageData(data); 41 | setLoading(false); 42 | } catch (error: unknown) { 43 | console.log('Data fetching failed', error); 44 | } 45 | }; 46 | if (apiKey) fetchPageData(); 47 | }, [id, start, end]); 48 | 49 | return loading ? ( 50 | <> 51 | 52 | 53 | ) : ( 54 |
55 |
56 |
57 | 62 |
63 |

Request Summary

64 | 65 | 66 | 67 |
68 |
69 | 73 |
74 |
75 | 79 |
80 |
81 | 82 | 83 | ); 84 | }; 85 | 86 | export default PageDisplay; 87 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/PageLineChart.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LineChart, 3 | Line, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | Legend, 9 | Label, 10 | ResponsiveContainer, 11 | } from 'recharts'; 12 | 13 | const PageLineChart = ({ avgPageDurationsOverTime }) => { 14 | return ( 15 | <> 16 |

17 | Average Page Load Duration Over Time 18 |

19 | 20 | 29 | 30 | 31 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default PageLineChart; 52 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/SpanLineChart.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LineChart, 3 | Line, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | Legend, 9 | Label, 10 | ResponsiveContainer, 11 | } from 'recharts'; 12 | 13 | const SpanLineChart = ({ avgActionDurationsOverTime }) => { 14 | const colors = [ 15 | '#006CD1', 16 | '#f95d6a', 17 | '#d45087', 18 | 19 | '#ff7c43', 20 | '#665191', 21 | '#f95d6a', 22 | '#2f4b7c', 23 | '#a05195', 24 | ]; 25 | 26 | const actions = Object.keys(avgActionDurationsOverTime[0]).filter( 27 | (el) => el !== 'period', 28 | ); 29 | 30 | const lines = actions.map((action, i) => { 31 | return ( 32 | 41 | ); 42 | }); 43 | 44 | return ( 45 | <> 46 |

47 | Average Action Duration Over Time 48 |

49 | 50 | 59 | 60 | 61 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default SpanLineChart; 73 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/Table.tsx: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | const Table = ({ overallPageData }) => { 4 | const tableData = []; 5 | for (let i = 0; i < overallPageData.length; i++) { 6 | tableData.push( 7 | 8 | 16 | 19 | 22 | 25 | 28 | , 29 | ); 30 | } 31 | 32 | return ( 33 |
12 |
13 | {overallPageData[i].Name} 14 |
15 |
17 | {overallPageData[i]['Avg. duration (ms)'] + 'ms'} 18 | 20 | {overallPageData[i].Kind} 21 | 23 | {overallPageData[i]['Total no. of traces']} 24 | 26 | {overallPageData[i]['Total no. of executions']} 27 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {tableData} 44 |
NameAvg. DurationKindNo. of TracesNo. of Executions
45 | ); 46 | }; 47 | 48 | export default Table; 49 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/PageDisplay/Textbox.tsx: -------------------------------------------------------------------------------- 1 | import Box from '../../../../components/Box'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | const Textbox = ({ traceCount, overallAvg }) => { 5 | const boxData = [ 6 | { title: 'Average Page Load Duration', data: overallAvg }, 7 | { title: 'Total No. of Traces', data: traceCount }, 8 | ]; 9 | 10 | return ( 11 |
12 | {boxData.map(({ title, data }) => ( 13 | 14 | ))} 15 |
16 | ); 17 | }; 18 | 19 | export default Textbox; 20 | -------------------------------------------------------------------------------- /src/pages/Dashboard/MainDisplay/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef, useState } from 'react'; 2 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 3 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 4 | import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; 5 | import dayjs from 'dayjs'; 6 | import Button from '../../../components/Button'; 7 | import { CopyInput } from '../../../components/CopyInput'; 8 | import { APIContext } from '../../../contexts/dashboardContexts'; 9 | // import { IoLogOut, IoMenu } from 'react-icons/io5'; 10 | // import { UserContext } from '../../../contexts/userContexts'; 11 | // import { Link, useNavigate } from 'react-router-dom'; 12 | // import logo from '../../../assets/NextView-logo-pink-transparent.webp'; 13 | // import PageTab from '../Sidebar/PageTab'; 14 | // import { v4 as uuidv4 } from 'uuid'; 15 | 16 | const Topbar = ({ setStart, setEnd, overviewData }) => { 17 | const [startVal, setStartVal] = useState(''); 18 | const [endVal, setEndVal] = useState(''); 19 | const [dropdown, setDropdown] = useState(false); 20 | 21 | const { apiKey } = useContext(APIContext); 22 | 23 | // Set local variables (startVal, endVal) 24 | const handleDateChange = (date, setDate) => { 25 | setDate(date.toISOString()); 26 | }; 27 | 28 | // Set Dashboard state variables (start, end) 29 | const handleClick = () => { 30 | setStart(startVal); 31 | setEnd(endVal); 32 | }; 33 | 34 | // const pagesList = overviewData.pages; 35 | 36 | function useOutsideAlerter(ref) { 37 | useEffect(() => { 38 | /** 39 | * Alert if clicked on outside of element 40 | */ 41 | function handleClickOutside(event) { 42 | if (ref.current && !ref.current.contains(event.target)) { 43 | setTimeout(() => setDropdown(false), 100); 44 | } 45 | } 46 | // Bind the event listener 47 | document.addEventListener('mousedown', handleClickOutside); 48 | return () => { 49 | // Unbind the event listener on clean up 50 | document.removeEventListener('mousedown', handleClickOutside); 51 | }; 52 | }, [ref]); 53 | } 54 | 55 | const wrapperRef = useRef(null); 56 | useOutsideAlerter(wrapperRef); 57 | 58 | return ( 59 | 60 |
61 |
62 | 63 | API Key: 64 | 65 | 69 | {apiKey} 70 | 71 | {/* 75 | nextview-logo 76 | 77 |
78 | setDropdown(true)} 82 | /> 83 | {dropdown ? ( 84 |
88 | 96 | Pages 97 | {pagesList.toReversed().map((page) => ( 98 | 99 | ))} 100 |
101 | ) : ( 102 | <> 103 | )} 104 |
*/} 105 |
106 |
107 | { 112 | handleDateChange(value, setStartVal); 113 | }} 114 | /> 115 | { 120 | handleDateChange(value, setEndVal); 121 | }} 122 | /> 123 | 130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default Topbar; 137 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Sidebar/MainNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import PageTab from './PageTab'; 4 | import Button from '../../../components/Button'; 5 | import { useContext } from 'react'; 6 | import { PageContext } from '../../../contexts/dashboardContexts'; 7 | 8 | function MainNavBar({ overviewData }) { 9 | const pagesList = overviewData.pages; 10 | const { setPage } = useContext(PageContext); 11 | 12 | return ( 13 |
14 | 15 | 22 | 23 | 24 | Pages 25 | 26 | {pagesList 27 | .slice() 28 | .reverse() 29 | .map((page) => ( 30 | 31 | ))} 32 |
33 | ); 34 | } 35 | 36 | export default MainNavBar; 37 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Sidebar/PageTab.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | import { PageContext } from '../../../contexts/dashboardContexts'; 3 | import { useContext } from 'react'; 4 | 5 | function PageTab({ pageSelection }) { 6 | const { page, setPage } = useContext(PageContext); 7 | 8 | return ( 9 | setPage(pageSelection)} 11 | to={`/dashboard/page/${pageSelection._id}`} 12 | className={({ isActive }) => 13 | isActive 14 | ? 'ml-3 mr-3 w-11/12 rounded border border-t-0 bg-gray-200 px-6 py-2 text-left text-sm font-semibold drop-shadow-sm' 15 | : 'ml-3 mr-3 w-11/12 rounded border-b px-6 py-2 text-left text-sm hover:bg-gray-100' 16 | } 17 | > 18 | {pageSelection.page} 19 | 20 | ); 21 | } 22 | 23 | export default PageTab; 24 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Sidebar/SideNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from 'react-router-dom'; 2 | // import { IoSettingsSharp, IoInvertModeSharp} from 'react-icons/io5'; 3 | import { IoLogOut } from 'react-icons/io5'; 4 | // import { SiCodereview } from 'react-icons/si'; 5 | // import { MdDashboardCustomize } from 'react-icons/md'; 6 | import { useContext } from 'react'; 7 | import { UserContext } from '../../../contexts/userContexts'; 8 | import handleLogOutHelper from '../../../../helpers/handleLogOutHelper'; 9 | 10 | function SideNavBar() { 11 | const { setLoggedIn } = useContext(UserContext); 12 | const navigate = useNavigate(); 13 | 14 | const handleLogOut = () => handleLogOutHelper(setLoggedIn, navigate); 15 | 16 | return ( 17 |
18 |
19 | 20 | nextview-logo 27 | 28 | {/* 29 | } /> 30 | */} 31 | 32 | } /> 33 | 34 |
35 | 36 | {/* stretch features 37 | 38 | } /> 39 | 40 | 41 | } /> 42 | 43 | 44 | } /> 45 | */} 46 |
47 | ); 48 | } 49 | 50 | const SideNavBarIcon = ({ icon }: { icon: React.ReactElement }) => ( 51 |
{icon}
52 | ); 53 | 54 | export default SideNavBar; 55 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import MainNavbar from './MainNavbar'; 2 | import SideNavbar from './SideNavbar'; 3 | 4 | const Sidebar = ({ overviewData }) => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Sidebar; 14 | -------------------------------------------------------------------------------- /src/pages/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from './Dashboard'; 2 | 3 | const DashboardPage = () => { 4 | return ; 5 | }; 6 | 7 | export default DashboardPage; 8 | -------------------------------------------------------------------------------- /src/pages/Home/Auth/AuthContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import Button from '../../../components/Button'; 3 | import Modal from './Modal'; 4 | import LoginForm from './LoginForm'; 5 | import SignupForm from './SignupForm'; 6 | import { UserContext } from '../../../contexts/userContexts'; 7 | import { useNavigate } from 'react-router-dom'; 8 | import handleLogOutHelper from '../../../../helpers/handleLogOutHelper'; 9 | 10 | const AuthContainer = () => { 11 | const [openLoginModal, setOpenLoginModal] = useState(false); 12 | const [openSignupModal, setOpenSignupModal] = useState(false); 13 | const { loggedIn, setLoggedIn } = useContext(UserContext); 14 | const navigate = useNavigate(); 15 | 16 | const handleLogOut = () => handleLogOutHelper(setLoggedIn, navigate); 17 | 18 | return ( 19 |
20 |
    21 |
  • 22 | {loggedIn ? ( 23 | 26 | ) : ( 27 | 33 | )} 34 |
  • 35 |
  • 36 | {loggedIn ? ( 37 | 44 | ) : ( 45 | 52 | )} 53 |
  • 54 |
55 | { 58 | setOpenLoginModal(false); 59 | }} 60 | > 61 | 62 | 63 | { 66 | setOpenSignupModal(false); 67 | }} 68 | > 69 | 70 | 71 |
72 | ); 73 | }; 74 | 75 | export default AuthContainer; 76 | -------------------------------------------------------------------------------- /src/pages/Home/Auth/AuthForm.tsx: -------------------------------------------------------------------------------- 1 | import Button from '../../../components/Button'; 2 | import { UserContext } from '../../../contexts/userContexts'; 3 | import React, { useContext, ChangeEvent, useCallback } from 'react'; 4 | 5 | interface AuthFormProps { 6 | usernameInputId: string; 7 | passwordInputId: string; 8 | text: string; 9 | handleSubmit: (e: FormEvent) => void; 10 | value?: string; 11 | } 12 | 13 | const AuthForm = React.memo( 14 | ({ 15 | usernameInputId, 16 | passwordInputId, 17 | text, 18 | handleSubmit, 19 | value, 20 | }: AuthFormProps) => { 21 | const { setPassword, setUsername } = useContext(UserContext); 22 | 23 | const handleUsernameChange = useCallback( 24 | (e: ChangeEvent) => { 25 | setUsername(e.target.value); 26 | }, 27 | [], 28 | ); 29 | 30 | const handlePasswordChange = useCallback( 31 | (e: ChangeEvent) => { 32 | setPassword(e.target.value); 33 | }, 34 | [], 35 | ); 36 | 37 | return ( 38 | <> 39 |

40 | {text} 41 |

42 |
43 |
44 | 50 | 60 |
61 |
62 | 68 | 76 |
77 |
78 | 81 |
82 |
83 | 84 | ); 85 | }, 86 | ); 87 | 88 | export default AuthForm; 89 | -------------------------------------------------------------------------------- /src/pages/Home/Auth/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import AuthForm from './AuthForm'; 4 | import { UserContext } from '../../../contexts/userContexts'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | const Login = () => { 8 | const { setLoggedIn, username, password } = useContext(UserContext); 9 | 10 | const navigate = useNavigate(); 11 | const uniqueId = uuidv4(); 12 | 13 | const handleSubmit = React.useCallback( 14 | (e: FormEvent) => { 15 | e.preventDefault(); 16 | 17 | fetch('/user/login', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'Application/JSON', 21 | }, 22 | body: JSON.stringify({ 23 | username, 24 | password, 25 | }), 26 | }) 27 | .then((res) => { 28 | if (!res.ok) { 29 | throw new Error('Log in unsuccessful. Please retry.' + res.status); 30 | } 31 | return res.json(); 32 | }) 33 | .then((res) => { 34 | if (res.user) { 35 | localStorage.setItem('user', JSON.stringify(res.user)); 36 | setLoggedIn(true); 37 | navigate('/dashboard'); 38 | } 39 | }) 40 | .catch((err) => console.log('Log in: ERROR: ', err)); 41 | }, 42 | [navigate, password, setLoggedIn, username], 43 | ); 44 | 45 | return ( 46 | 52 | ); 53 | }; 54 | 55 | export default Login; 56 | -------------------------------------------------------------------------------- /src/pages/Home/Auth/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useLayoutEffect, useRef } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | open: boolean; 6 | onClose: () => void; 7 | } 8 | 9 | const Modal = ({ children, open, onClose }: Props) => { 10 | const ref = useRef(null); 11 | 12 | useLayoutEffect(() => { 13 | const closeListenerFnc = () => { 14 | onClose && onClose(); 15 | }; 16 | 17 | const dialogRef = ref.current; 18 | dialogRef?.addEventListener('close', closeListenerFnc); 19 | 20 | return () => { 21 | dialogRef?.removeEventListener('close', closeListenerFnc); 22 | }; 23 | }, [onClose]); 24 | 25 | useLayoutEffect(() => { 26 | if (open && !ref.current?.open) { 27 | ref.current?.showModal(); 28 | } else if (!open && ref.current?.open) { 29 | ref.current?.close(); 30 | } 31 | }, [open]); 32 | 33 | return ( 34 | { 38 | const dialogDimensions = e.currentTarget.getBoundingClientRect(); 39 | if ( 40 | e.clientX < dialogDimensions.left || 41 | e.clientX > dialogDimensions.right || 42 | e.clientY < dialogDimensions.top || 43 | e.clientY > dialogDimensions.bottom 44 | ) { 45 | e.currentTarget.close(); 46 | } 47 | }} 48 | > 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | export default Modal; 55 | -------------------------------------------------------------------------------- /src/pages/Home/Auth/SignupForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useContext } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import AuthForm from './AuthForm'; 4 | import { UserContext } from '../../../contexts/userContexts'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | import validateStrongPassword from '../../../../helpers/validateStrongPassword'; 7 | 8 | const Signup = () => { 9 | const { setLoggedIn, username, password } = useContext(UserContext); 10 | 11 | const navigate = useNavigate(); 12 | const uniqueId = uuidv4(); 13 | 14 | function addApp() { 15 | fetch('/apps', { 16 | method: 'POST', 17 | headers: { 18 | 'content-type': 'application/json', 19 | }, 20 | }) 21 | .then((res) => { 22 | if (!res.ok) { 23 | throw new Error(); 24 | } 25 | }) 26 | .catch((err) => console.log('Add app ERROR: ', err)); 27 | } 28 | 29 | const handleSubmit = React.useCallback( 30 | (e: FormEvent) => { 31 | e.preventDefault(); 32 | 33 | if (username.length < 3) { 34 | alert('Username length must be at least 3!'); 35 | return; 36 | } 37 | // validate strong password 38 | const strongPassword = validateStrongPassword(password); 39 | 40 | if (!strongPassword) { 41 | alert( 42 | 'The password length must be greater than or equal to 8, contains at least one uppercase character, one lowercase character, one numeric value, and one special characters of !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', 43 | ); 44 | return; 45 | } 46 | 47 | const body = { 48 | username, 49 | password, 50 | }; 51 | 52 | fetch('/user/register', { 53 | method: 'POST', 54 | headers: { 55 | 'Content-Type': 'Application/JSON', 56 | }, 57 | body: JSON.stringify({ 58 | username, 59 | password, 60 | }), 61 | }) 62 | .then((res) => { 63 | if (!res.ok) { 64 | throw new Error( 65 | 'Registration unsuccessful. Please retry.' + res.status, 66 | ); 67 | } 68 | return res.json(); 69 | }) 70 | .then((res) => { 71 | if (res.user) { 72 | localStorage.setItem('user', JSON.stringify(res.user)); 73 | setLoggedIn(true); 74 | addApp(); 75 | navigate('/dashboard'); 76 | } 77 | }) 78 | .catch((err) => console.log('Sign up ERROR: ', err)); 79 | }, 80 | [navigate, password, setLoggedIn, username], 81 | ); 82 | 83 | return ( 84 | 91 | ); 92 | }; 93 | 94 | export default Signup; 95 | -------------------------------------------------------------------------------- /src/pages/Home/Contributor.tsx: -------------------------------------------------------------------------------- 1 | interface ContributorProps { 2 | name: string; 3 | image: string; 4 | linkedinLink: string; 5 | githubLink: string; 6 | } 7 | 8 | const Contributor: React.FC = ({ 9 | name, 10 | image, 11 | linkedinLink, 12 | githubLink, 13 | }) => { 14 | return ( 15 |
16 | 17 |
18 | {name} 19 | Software Engineer 20 |
21 | 45 |
46 | ); 47 | }; 48 | 49 | export default Contributor; 50 | -------------------------------------------------------------------------------- /src/pages/Home/Contributors.tsx: -------------------------------------------------------------------------------- 1 | import Contributor from './Contributor'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | const contributorsData = [ 5 | { 6 | name: 'Eduardo Zayas', 7 | image: 8 | 'https://ik.imagekit.io/4ys419c44/eduardo.webp?updatedAt=1692136903252', 9 | linkedinLink: 'https://www.linkedin.com/in/eduardo-zayas-avila/', 10 | githubLink: 'https://github.com/eza16', 11 | }, 12 | { 13 | name: 'Evram Dawd', 14 | image: 15 | 'https://ik.imagekit.io/4ys419c44/evram.webp?updatedAt=1692136903569', 16 | linkedinLink: 'https://www.linkedin.com/in/evram-d-905a3a2b/', 17 | githubLink: 'https://github.com/evramdawd', 18 | }, 19 | { 20 | name: 'Kinski (Jiaxin) Wu', 21 | image: 22 | 'https://ik.imagekit.io/4ys419c44/kinski.webp?updatedAt=1692136903472', 23 | linkedinLink: 'https://www.linkedin.com/in/kinskiwu/', 24 | githubLink: 'https://github.com/kinskiwu', 25 | }, 26 | { 27 | name: 'Scott Brasko', 28 | image: 29 | 'https://ik.imagekit.io/4ys419c44/scott.webp?updatedAt=1692136903515', 30 | linkedinLink: 'https://www.linkedin.com/in/scott-brasko/', 31 | githubLink: 'https://github.com/Scott-Brasko', 32 | }, 33 | { 34 | name: 'SooJi Kim', 35 | image: 36 | 'https://ik.imagekit.io/4ys419c44/sooji.webp?updatedAt=1692136903364', 37 | linkedinLink: 'https://www.linkedin.com/in/sooji-suzy-kim/', 38 | githubLink: 'https://github.com/sjk06', 39 | }, 40 | ]; 41 | 42 | const Contributors: React.FC = () => { 43 | return ( 44 |
45 |
46 |

Contributors

47 | 53 |
54 |
55 | {contributorsData.map((contributor) => ( 56 | 63 | ))} 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Contributors; 70 | -------------------------------------------------------------------------------- /src/pages/Home/Features.tsx: -------------------------------------------------------------------------------- 1 | import Feature from '../../components/Feature'; 2 | import Button from '../../components/Button'; 3 | import { useEffect, useState } from 'react'; 4 | import { FaStar, FaRegStar } from 'react-icons/fa'; 5 | import { HiOutlineWrenchScrewdriver, HiOutlineChartBar } from 'react-icons/hi2'; 6 | import { IoTelescopeOutline } from 'react-icons/io5'; 7 | import { AiOutlineSecurityScan } from 'react-icons/ai'; 8 | 9 | const Features = () => { 10 | const [starCount, setstarCount] = useState(0); 11 | const [isHovering, setIsHovering] = useState(false); 12 | const handleMouseOver = () => { 13 | setIsHovering(true); 14 | }; 15 | const handleMouseOut = () => { 16 | setIsHovering(false); 17 | }; 18 | 19 | const getStars = () => { 20 | fetch('https://api.github.com/repos/oslabs-beta/NextView', { 21 | method: 'GET', 22 | }) 23 | .then((res) => { 24 | if (!res.ok) { 25 | throw new Error('HTTP error ' + res.status); 26 | } 27 | return res.json(); 28 | }) 29 | .then((res) => { 30 | if (res.stargazers_count) { 31 | setstarCount(res.stargazers_count); 32 | } 33 | }) 34 | .catch((err) => console.log('Authenticate: ERROR: ', err)); 35 | }; 36 | 37 | useEffect(() => getStars(), []); 38 | 39 | return ( 40 |
41 |
42 | 49 |

50 | What is NextView? ⭐🔭 51 |

52 |
53 |

54 | NextView is an observability platform for building and optimizing 55 | Next.js applications. NextView assists developers by providing an 56 | easy-to-use and lightweight toolkit for measuring performance of 57 | server-side rendering requests. 58 |

59 |
60 |
61 | 62 |
63 | 64 | 65 |

Next.js Instrumentation

66 |

67 | With our hassle-free npm package integration, effortlessly track and 68 | analyze trace data in your Next.js application, empowering you to 69 | gain valuable insights and optimize performance in no time. 70 |

71 |
72 | 73 | 74 |

Traffic Analysis

75 |

76 | Gain insights into your application's usage patterns, identify 77 | trends, and make informed decisions for optimizing your server-side 78 | rendering infrastructure. 79 |

80 |
81 | 82 | 83 |

Performance Monitoring

84 |

85 | By providing detailed performance data over customizable 86 | time-ranges, you can identify performance bottlenecks and optimize 87 | your applications for better user experience. 88 |

89 |
90 | 91 | 92 |

Secure Authentication

93 |

94 | Safeguard your data with our robust encryption using bcrypt. Rest 95 | easy knowing that sensitive user information is securely stored, 96 | providing peace of mind and protection against unauthorized access. 97 |

98 |
99 |
100 | 155 |
156 | ); 157 | }; 158 | 159 | export default Features; 160 | -------------------------------------------------------------------------------- /src/pages/Home/Installation.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useContext } from 'react'; 2 | import Button from '../../components/Button'; 3 | import { CopyInput } from '../../components/CopyInput'; 4 | import { UserContext } from '../../contexts/userContexts'; 5 | import { NPMCopyInput } from '../../components/NPMCopyInput'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import { APIContext } from '../../contexts/dashboardContexts'; 8 | 9 | interface Props { 10 | setOpenSignupModal(value: React.SetStateAction): void; 11 | } 12 | 13 | const Installation: React.FC = ({ setOpenSignupModal }) => { 14 | const { loggedIn, setUsername } = useContext(UserContext); 15 | const { apiKey } = useContext(APIContext); 16 | const handleUsernameChange = (e: ChangeEvent) => { 17 | setUsername(e.target.value); 18 | }; 19 | 20 | const navigate = useNavigate(); 21 | return ( 22 |
23 |
24 |

Get Started

25 |
26 |
    27 |
  1. 28 |

    29 | To get started, install our npm package in your Next.js 30 | application: 31 |

    32 | 33 |
    34 |
    35 | 41 | npm-logo 48 | 49 |
    50 | 51 |
    52 |
  2. 53 |
  3. 54 |

    55 | In your next.config.js file, opt-in to the Next.js 56 | instrumentation by setting the experimental instrumentationHook 57 | to true: 58 |

    59 | 60 |
    61 | 62 | experimental.instrumentationHook = true; 63 | 64 |
    65 |
  4. 66 | {loggedIn ? ( 67 | <> 68 | ) : ( 69 |
  5. 70 |

    Register a NextView account here:

    71 | 80 | 88 |
  6. 89 | )} 90 |
  7. 91 |

    92 | Navigate to the NextView Dashboard and copy your generated API 93 | key. 94 |

    95 | {loggedIn ? ( 96 | 103 | ) : ( 104 | <> 105 | )} 106 |
  8. 107 |
  9. 108 |

    109 | In the .env.local{' '} 110 | file in the root directory of your application (create one if it 111 | doesn’t exist), create an environment variable for your API Key. 112 |

    113 |
    114 | `}> 115 | {'API_KEY='} 116 | 117 |
    118 |
  10. 119 |
120 |
121 |
122 | 128 |

129 | You're all set up! You can monitor the server operations in your 130 | Next.js application on the NextView Dashboard! 131 |

132 | 138 |
139 | {loggedIn ? ( 140 | <> 141 | 148 | 149 | ) : ( 150 | <> 151 | )} 152 |
153 | 160 |
161 |
162 |
163 | ); 164 | }; 165 | 166 | export default Installation; 167 | -------------------------------------------------------------------------------- /src/pages/Home/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import AuthContainer from './Auth/AuthContainer'; 2 | 3 | interface Props { 4 | installationScroll(): void; 5 | contributorsScroll(): void; 6 | } 7 | 8 | const Navbar: React.FC = ({ 9 | installationScroll, 10 | contributorsScroll, 11 | }) => { 12 | return ( 13 |
14 |
18 | 19 | nextview-logo 26 | 27 |
28 |
32 | Get Started 33 |
34 |
35 |
36 |
40 | Contributors 41 |
42 |
43 | 54 |
55 | 61 | github-logo 69 | 70 |
71 |
72 | 78 | npm-logo 85 | 86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | export default Navbar; 98 | -------------------------------------------------------------------------------- /src/pages/Home/Overview.tsx: -------------------------------------------------------------------------------- 1 | import Button from '../../components/Button'; 2 | import { ChangeEvent, useContext } from 'react'; 3 | import { TypeAnimation } from 'react-type-animation'; 4 | import { UserContext } from '../../contexts/userContexts'; 5 | import React from 'react'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import { NPMCopyInput } from '../../components/NPMCopyInput'; 8 | 9 | const sequence = [ 10 | 'Build', 11 | 2000, 12 | 'Analyze', 13 | 2000, 14 | 'Debug', 15 | 2000, 16 | 'Develop', 17 | 2000, 18 | 'Trace', 19 | 2000, 20 | 'Create', 21 | 2000, 22 | 'Visualize', 23 | 2000, 24 | 'Test', 25 | 2000, 26 | 'Instrument', 27 | 2000, 28 | 'Measure', 29 | 2000, 30 | 'Optimize ', 31 | () => showCursorAnimation(false), 32 | ]; 33 | const CURSOR_CLASS_NAME = 'type'; 34 | const ref = React.createRef(); 35 | const showCursorAnimation = (show: boolean) => { 36 | if (!ref.current) { 37 | return; 38 | } 39 | 40 | const el = ref.current; 41 | if (show) { 42 | el.classList.add(CURSOR_CLASS_NAME); 43 | } else { 44 | el.classList.remove(CURSOR_CLASS_NAME); 45 | } 46 | }; 47 | 48 | interface Props { 49 | setOpenSignupModal(value: React.SetStateAction): void; 50 | } 51 | 52 | const Overview: React.FC = ({ setOpenSignupModal }) => { 53 | const { setUsername, username, loggedIn } = useContext(UserContext); 54 | 55 | const handleUsernameChange = (e: ChangeEvent) => { 56 | setUsername(e.target.value); 57 | }; 58 | const navigate = useNavigate(); 59 | 60 | return ( 61 |
65 |
66 |

NextView

67 | 68 | 75 | your Next.js application 76 | 77 | 78 |
79 | {loggedIn ? ( 80 | <> 81 |
82 | Welcome {username}! 83 |
84 | 91 | 92 | ) : ( 93 | <> 94 | 103 | 111 | 112 | )} 113 |
114 |
115 | 116 |
117 |
118 |
119 | nextview-logo 126 |
127 |
128 | ); 129 | }; 130 | 131 | export default Overview; 132 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext, useEffect, useRef, useState } from 'react'; 2 | import Navbar from './Navbar'; 3 | import Overview from './Overview'; 4 | import { UserContext } from '../../contexts/userContexts'; 5 | import Features from './Features'; 6 | import SignupForm from './Auth/SignupForm'; 7 | import Modal from './Auth/Modal'; 8 | import Installation from './Installation'; 9 | import Contributors from './Contributors'; 10 | 11 | const Home = () => { 12 | const { setLoggedIn, setUsername } = useContext(UserContext); 13 | const [openSignupModal, setOpenSignupModal] = useState(false); 14 | 15 | const installationRef = useRef(null); 16 | const contributorsRef = useRef(null); 17 | const executeScroll = (ref: React.RefObject) => { 18 | return () => { 19 | if (ref.current) ref.current.scrollIntoView({ behavior: 'smooth' }); 20 | }; 21 | }; 22 | 23 | const checkLogin = useCallback(() => { 24 | fetch('/user/authenticate', { 25 | method: 'GET', 26 | }) 27 | .then((res) => { 28 | if (!res.ok) { 29 | throw new Error('HTTP error ' + res.status); 30 | } 31 | return res.json(); 32 | }) 33 | .then((res) => { 34 | if (res.username) { 35 | localStorage.setItem('user', JSON.stringify(res.username)); 36 | setLoggedIn(true); 37 | setUsername(res.username); 38 | } 39 | }) 40 | .catch((err) => console.log('Authenticate: ERROR: ', err)); 41 | }, [setLoggedIn, setUsername]); 42 | 43 | useEffect(() => checkLogin(), [checkLogin]); 44 | 45 | return ( 46 | <> 47 | 51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 | { 63 | setOpenSignupModal(false); 64 | }} 65 | > 66 | 67 | 68 |
69 | 70 | ); 71 | }; 72 | 73 | export default Home; 74 | -------------------------------------------------------------------------------- /src/pages/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | const NotFound = () => { 4 | const navigate = useNavigate(); 5 | 6 | return ( 7 |
8 |
9 |
10 |

11 | 404 error 12 |

13 |

14 | Page not found 15 |

16 |

17 | Sorry, the page you are looking for doesn't exist.Here are some 18 | helpful links: 19 |

20 | 21 |
22 | 39 | 40 | 46 |
47 |
48 | 49 |
50 | 55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default NotFound; 62 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: 'var(--primary-color)', 8 | secondary: 'var(--secondary-color)', 9 | accent: 'var(--accent-color)', 10 | }, 11 | screens: { 12 | '3xl': '2000px', 13 | wrap: '1461px', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "CommonJS", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "typeRoots": ["./types", "./node_modules/@types" ], 16 | "allowSyntheticDefaultImports": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "composite": true, 11 | "typeRoots": ["./types"], 12 | "strict": true, 13 | "allowSyntheticDefaultImports": true, 14 | }, 15 | "include": ["vite.config.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import { JwtPayload } from 'jsonwebtoken'; 2 | 3 | declare global { 4 | namespace Express { 5 | interface Request { 6 | user?: string | UserJwtPayload; 7 | userId: bigint; 8 | } 9 | } 10 | } 11 | 12 | declare module 'jsonwebtoken' { 13 | export interface UserJwtPayload extends jwt.JwtPayload { 14 | userId: bigint; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import gzipPlugin from 'rollup-plugin-gzip'; 4 | /* 5 | * Use this if we want to HMR the server, but lose global error handling/global 404 6 | * The Express app plugin. Specify the URL base path 7 | * for the app and the Express app object. 8 | */ 9 | // import server from './server/server' 10 | // const expressServerPlugin = (path: string, expressApp) => ({ 11 | // name: 'configure-server', 12 | // configureServer(server) { 13 | // return () => { 14 | // server.middlewares.use(path, expressApp); 15 | // } 16 | // } 17 | // }); 18 | 19 | /* 20 | * Vite configuration 21 | */ 22 | export default defineConfig({ 23 | server: { 24 | proxy: { 25 | '/user': 'http://localhost:3000', 26 | '/apps': 'http://localhost:3000', 27 | '/api': 'http://localhost:3000', 28 | }, 29 | }, 30 | plugins: [ 31 | react(), 32 | gzipPlugin(), 33 | // expressServerPlugin('/', server) 34 | ], 35 | build: { 36 | rollupOptions: { 37 | output: { 38 | manualChunks: (id) => { 39 | // group all react-realted modules into a 'react' chunk 40 | if (id.includes('node_modules/react')) { 41 | return 'react'; 42 | } 43 | 44 | // group all recharts-realted modules into a 'recharts' chunk 45 | if (id.includes('node_modules/recharts')) { 46 | return 'recharts'; 47 | } 48 | }, 49 | }, 50 | }, 51 | }, 52 | }); 53 | --------------------------------------------------------------------------------