27 |
28 | #
29 |
30 | 
31 | 
32 | 
33 | 
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 | 
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 | 
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 | 
4 |
5 |
25 |
26 | #
27 |
28 | 
29 | 
30 | 
31 | 
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 |
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 |
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 |
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 |