├── .browserslistrc ├── .dockerignore ├── .env ├── .env.local ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── CHANGE_REQUEST.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml ├── workflows │ ├── dependabot-automerge.yml │ ├── pull-request.yml │ ├── release.yml │ └── semantic-pr.yml └── zizmor.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .madgerc ├── .markdownlint.json ├── .markdownlintrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .yarn └── releases │ └── yarn-1.22.19.cjs ├── .yarnrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── GOVERNANCE.md ├── ISSUE_TRIAGE.md ├── LICENSE ├── LICENSING.md ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── cypress.config.ts ├── cypress ├── e2e │ └── demo │ │ ├── errors.cy.ts │ │ ├── events.cy.ts │ │ ├── logs.cy.ts │ │ ├── measurements.cy.ts │ │ └── tracing.cy.ts ├── support │ ├── commands.ts │ └── e2e.ts └── tsconfig.json ├── dashboards ├── app-agent-receiver.json └── frontend-application.json ├── demo ├── README.md ├── docs │ ├── assets │ │ ├── instrumentations │ │ │ ├── errorsViewDashboard.png │ │ │ ├── errorsViewExplore.png │ │ │ ├── eventsViewExplore.png │ │ │ ├── eventsViewExploreTraces.png │ │ │ ├── logsViewExplore.png │ │ │ ├── measurementsViewDashboard.png │ │ │ ├── measurementsViewExploreLoki.png │ │ │ ├── measurementsViewExploreMimir.png │ │ │ ├── metasViewApp.png │ │ │ ├── metasViewExploreSession.png │ │ │ ├── metasViewExploreUser.png │ │ │ ├── tracesViewAppDocumentLoad.png │ │ │ ├── tracesViewAppSeed.png │ │ │ ├── tracesViewExploreDocumentLoad.png │ │ │ └── tracesViewExploreSeed.png │ │ └── userJourney │ │ │ ├── homepage.png │ │ │ ├── homepageTraces.png │ │ │ ├── homepageWebVitals.png │ │ │ ├── registerError.png │ │ │ ├── registerErrorEvents.png │ │ │ ├── registerErrorTraces.png │ │ │ ├── seed.png │ │ │ ├── seedError.png │ │ │ ├── seedErrorEvents.png │ │ │ ├── seedErrorTraces.png │ │ │ ├── seedEvents.png │ │ │ ├── seedSuccess.png │ │ │ ├── seedSuccessEvents.png │ │ │ └── seedSuccessTraces.png │ └── instrumentations │ │ ├── errors.md │ │ ├── events.md │ │ ├── logs.md │ │ ├── measurements.md │ │ ├── metas.md │ │ └── traces.md ├── index.html ├── logs │ └── .gitkeep ├── nodemon.json ├── package.json ├── public │ └── favicon.png ├── src │ ├── client │ │ ├── App │ │ │ ├── App.tsx │ │ │ ├── AuthWrapper.tsx │ │ │ └── index.ts │ │ ├── api │ │ │ ├── articles.ts │ │ │ ├── auth.ts │ │ │ ├── baseQuery.ts │ │ │ ├── index.ts │ │ │ ├── middleware.ts │ │ │ ├── reducers.ts │ │ │ └── seed.ts │ │ ├── components │ │ │ ├── LoadingScreen │ │ │ │ ├── LoadingScreen.tsx │ │ │ │ └── index.ts │ │ │ ├── Navbar │ │ │ │ ├── Navbar.tsx │ │ │ │ └── index.ts │ │ │ ├── Page │ │ │ │ ├── Page.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── faro │ │ │ ├── index.ts │ │ │ └── initialize.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useAppDispatch.ts │ │ │ ├── useAppSelector.ts │ │ │ └── useIsomorphicEffect.ts │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── layouts │ │ │ ├── GeneralLayout │ │ │ │ ├── GeneralLayout.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── pages │ │ │ ├── About │ │ │ │ ├── About.tsx │ │ │ │ └── index.ts │ │ │ ├── ArticleAdd │ │ │ │ ├── ArticleAdd.tsx │ │ │ │ ├── ArticleAddForm.tsx │ │ │ │ └── index.ts │ │ │ ├── ArticleView │ │ │ │ ├── ArticleView.tsx │ │ │ │ ├── CommentAddForm.tsx │ │ │ │ └── index.ts │ │ │ ├── Articles │ │ │ │ ├── Articles.tsx │ │ │ │ └── index.ts │ │ │ ├── Features │ │ │ │ ├── ConsoleInstrumentation.tsx │ │ │ │ ├── Counter.tsx │ │ │ │ ├── ErrorInstrumentation.tsx │ │ │ │ ├── Events.tsx │ │ │ │ ├── Features.tsx │ │ │ │ ├── MetricsMeasurements.tsx │ │ │ │ ├── ReactInstrumentation.tsx │ │ │ │ ├── TracingInstrumentation.tsx │ │ │ │ └── index.ts │ │ │ ├── Home │ │ │ │ ├── Home.tsx │ │ │ │ └── index.ts │ │ │ ├── Login │ │ │ │ ├── Login.tsx │ │ │ │ ├── LoginForm.tsx │ │ │ │ └── index.ts │ │ │ ├── Register │ │ │ │ ├── Register.tsx │ │ │ │ ├── RegisterForm.tsx │ │ │ │ └── index.ts │ │ │ ├── Seed │ │ │ │ ├── Seed.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── router │ │ │ ├── Router.tsx │ │ │ ├── guards │ │ │ │ ├── LoggedInGuard.tsx │ │ │ │ ├── LoggedOutGuard.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── slices │ │ │ │ ├── faro.ts │ │ │ │ ├── index.ts │ │ │ │ └── user.ts │ │ │ └── store.ts │ │ └── utils │ │ │ ├── env.ts │ │ │ ├── formatDate.ts │ │ │ ├── index.ts │ │ │ └── reduxToolkit.ts │ ├── common │ │ ├── index.ts │ │ ├── types │ │ │ ├── api │ │ │ │ ├── article.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── generic.ts │ │ │ │ ├── index.ts │ │ │ │ └── seed.ts │ │ │ ├── index.ts │ │ │ └── models │ │ │ │ ├── article.ts │ │ │ │ ├── comment.ts │ │ │ │ ├── index.ts │ │ │ │ └── user.ts │ │ └── utils │ │ │ ├── env.ts │ │ │ └── index.ts │ ├── server │ │ ├── app │ │ │ ├── index.ts │ │ │ └── initialize.ts │ │ ├── db │ │ │ ├── db.ts │ │ │ ├── handlers │ │ │ │ ├── articles.ts │ │ │ │ ├── comments.ts │ │ │ │ ├── index.ts │ │ │ │ └── users.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── mock.ts │ │ │ └── repositories │ │ │ │ ├── article.ts │ │ │ │ ├── comment.ts │ │ │ │ ├── index.ts │ │ │ │ └── user.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ └── logger.ts │ │ ├── metrics │ │ │ ├── index.ts │ │ │ └── initialize.ts │ │ ├── middlewares │ │ │ ├── auth.ts │ │ │ ├── index.ts │ │ │ ├── serverTiming.ts │ │ │ ├── token.ts │ │ │ └── traceparent.ts │ │ ├── otel │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── span.ts │ │ │ └── tracer.ts │ │ ├── routes │ │ │ ├── api │ │ │ │ ├── articles │ │ │ │ │ ├── addArticleCommentHandler.ts │ │ │ │ │ ├── addArticleHandler.ts │ │ │ │ │ ├── getArticleHandler.ts │ │ │ │ │ ├── getArticlesHandler.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── registerApiArticlesRoutes.ts │ │ │ │ ├── auth │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── loginHandler.ts │ │ │ │ │ ├── logoutHandler.ts │ │ │ │ │ ├── registerApiAuthRoutes.ts │ │ │ │ │ ├── registerHandler.ts │ │ │ │ │ └── stateHandler.ts │ │ │ │ ├── index.ts │ │ │ │ ├── registerApiRoutes.ts │ │ │ │ └── seed │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── registerApiSeedRoutes.ts │ │ │ │ │ └── seedHandler.ts │ │ │ ├── index.ts │ │ │ ├── metrics │ │ │ │ ├── getMetricsHandler.ts │ │ │ │ ├── index.ts │ │ │ │ └── registerMetricsRoutes.ts │ │ │ ├── registerRoutes.ts │ │ │ └── render │ │ │ │ ├── index.ts │ │ │ │ ├── registerRenderRoutes.ts │ │ │ │ ├── renderDev.ts │ │ │ │ ├── renderPage.ts │ │ │ │ ├── renderProd.ts │ │ │ │ └── renderToString.tsx │ │ └── utils │ │ │ ├── const.ts │ │ │ ├── env.ts │ │ │ ├── index.ts │ │ │ ├── removeAuthorizationToken.ts │ │ │ ├── sendError.ts │ │ │ ├── sendFormValidationError.ts │ │ │ ├── sendSuccess.ts │ │ │ ├── sendUnauthenticatedError.ts │ │ │ ├── setAuthorizationToken.ts │ │ │ ├── signToken.ts │ │ │ ├── toAbsolutePath.ts │ │ │ ├── types.ts │ │ │ └── verifyToken.ts │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts ├── docker-compose.yaml ├── docs ├── README.md ├── faro_logo.png └── sources │ ├── developer │ ├── architecture │ │ ├── components │ │ │ ├── api.md │ │ │ ├── instrumentations.md │ │ │ ├── internal-logger.md │ │ │ ├── metas.md │ │ │ ├── transports.md │ │ │ └── unpatched-console.md │ │ └── initialization.md │ └── releasing.md │ └── tutorials │ ├── quick-start-browser.md │ ├── use-angular.md │ └── use-cdn-library.md ├── experimental ├── CHANGELOG.md ├── instrumentation-fetch │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── instrumentation.test.ts │ │ ├── instrumentation.ts │ │ ├── setupTests.ts │ │ └── types.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── instrumentation-performance-timeline │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── instrumentation.test.ts │ │ ├── instrumentation.ts │ │ └── types.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── instrumentation-xhr │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── instrumentation.test.ts │ │ ├── instrumentation.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── transport-otlp-http │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── index.ts │ ├── payload │ │ ├── OtelPayload.test.ts │ │ ├── OtelPayload.ts │ │ ├── attribute │ │ │ ├── attributeUtils.test.ts │ │ │ ├── attributeUtils.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── transform │ │ │ ├── index.ts │ │ │ ├── toErrorLogRecord.test.ts │ │ │ ├── toEventLogRecord.test.ts │ │ │ ├── toLogLogRecord.test.ts │ │ │ ├── toMeasurementLogRecord.test.ts │ │ │ ├── toResourceLog.test.ts │ │ │ ├── toResourceSpan.test.ts │ │ │ ├── toScopeLog.test.ts │ │ │ ├── transform.ts │ │ │ └── types.ts │ │ └── types.ts │ ├── semconv.ts │ ├── transport.test.ts │ ├── transport.ts │ └── types.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── infra ├── alloy │ └── config │ │ └── config.alloy ├── grafana │ ├── config │ │ └── grafana.ini │ ├── dashboards-provisioning │ │ └── dashboards.yaml │ ├── dashboards │ │ ├── frontend-application.json │ │ └── grafana-agent-receiver.json │ ├── datasources │ │ └── datasource.yaml │ └── plugins-provisioning │ │ └── .gitkeep ├── mimir │ └── config │ │ └── config.yaml ├── postgres │ └── config │ │ └── config.ini └── tempo │ └── config │ └── tempo.yaml ├── jest.config.base.js ├── lerna.json ├── package.json ├── packages ├── core │ ├── .browserslistrc │ ├── README.md │ ├── bin │ │ └── genVersion.js │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── api │ │ │ ├── ItemBuffer.ts │ │ │ ├── apiTestHelpers.ts │ │ │ ├── const.ts │ │ │ ├── events │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.test.ts │ │ │ │ ├── initialize.ts │ │ │ │ └── types.ts │ │ │ ├── exceptions │ │ │ │ ├── const.ts │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.test.ts │ │ │ │ ├── initialize.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── initialize.test.ts │ │ │ ├── initialize.ts │ │ │ ├── itemBuffer.test.ts │ │ │ ├── logs │ │ │ │ ├── const.ts │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.test.ts │ │ │ │ ├── initialize.ts │ │ │ │ └── types.ts │ │ │ ├── measurements │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.test.ts │ │ │ │ ├── initialize.ts │ │ │ │ └── types.ts │ │ │ ├── meta │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.ts │ │ │ │ ├── initilialize.test.ts │ │ │ │ └── types.ts │ │ │ ├── traces │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.ts │ │ │ │ └── types.ts │ │ │ ├── types.ts │ │ │ ├── userActionLifecycleHandler.test.ts │ │ │ ├── userActionLifecycleHandler.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── config │ │ │ ├── const.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── consts.ts │ │ ├── extensions │ │ │ ├── baseExtension.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── faro.test.ts │ │ ├── globalObject │ │ │ ├── globalObject.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── initialize.ts │ │ ├── instrumentations │ │ │ ├── base.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── registerInitial.ts │ │ │ └── types.ts │ │ ├── internalLogger │ │ │ ├── const.ts │ │ │ ├── createInternalLogger.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ └── types.ts │ │ ├── metas │ │ │ ├── index.ts │ │ │ ├── initialize.test.ts │ │ │ ├── initialize.ts │ │ │ ├── registerInitial.ts │ │ │ └── types.ts │ │ ├── sdk │ │ │ ├── const.ts │ │ │ ├── faroGlobalObject.ts │ │ │ ├── index.ts │ │ │ ├── internalFaroGlobalObject.ts │ │ │ ├── registerFaro.ts │ │ │ └── types.ts │ │ ├── semantic.ts │ │ ├── testUtils │ │ │ ├── index.ts │ │ │ ├── mockConfig.ts │ │ │ ├── mockInternalLogger.ts │ │ │ ├── mockStacktraceParser.ts │ │ │ ├── mockTransport.ts │ │ │ └── testPromise.ts │ │ ├── transports │ │ │ ├── base.ts │ │ │ ├── batchExecutor.test.ts │ │ │ ├── batchExecutor.ts │ │ │ ├── const.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── registerInitial.ts │ │ │ ├── transports.test.ts │ │ │ ├── types.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── unpatchedConsole │ │ │ ├── const.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ └── types.ts │ │ └── utils │ │ │ ├── baseObject.ts │ │ │ ├── date.ts │ │ │ ├── deepEqual.test.ts │ │ │ ├── deepEqual.ts │ │ │ ├── index.ts │ │ │ ├── is.test.ts │ │ │ ├── is.ts │ │ │ ├── json.test.ts │ │ │ ├── json.ts │ │ │ ├── logLevels.ts │ │ │ ├── noop.ts │ │ │ ├── promiseBuffer.test.ts │ │ │ ├── promiseBuffer.ts │ │ │ ├── reactive.test.ts │ │ │ ├── reactive.ts │ │ │ ├── shortId.ts │ │ │ ├── sourceMaps.test.ts │ │ │ └── sourceMaps.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── react │ ├── .browserslistrc │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── dependencies.ts │ │ ├── errorBoundary │ │ │ ├── FaroErrorBoundary.tsx │ │ │ ├── const.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── withFaroErrorBoundary.tsx │ │ ├── index.ts │ │ ├── instrumentation.ts │ │ ├── profiler │ │ │ ├── FaroProfiler.tsx │ │ │ ├── index.ts │ │ │ └── withFaroProfiler.tsx │ │ ├── router │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── types.ts │ │ │ ├── v4v5 │ │ │ │ ├── FaroRoute.tsx │ │ │ │ ├── activeEvent.ts │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.ts │ │ │ │ ├── routerDependencies.ts │ │ │ │ └── types.ts │ │ │ └── v6 │ │ │ │ ├── FaroRoutes.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── initialize.ts │ │ │ │ ├── routerDependencies.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.ts │ │ │ │ └── withFaroRouterInstrumentation.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── reactVersion.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── web-sdk │ ├── .browserslistrc │ ├── README.md │ ├── globals.ts │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── setup.jest.ts │ ├── src │ │ ├── config │ │ │ ├── getWebInstrumentations.ts │ │ │ ├── index.ts │ │ │ ├── makeCoreConfig.test.ts │ │ │ ├── makeCoreConfig.ts │ │ │ └── types.ts │ │ ├── consts.ts │ │ ├── index.ts │ │ ├── initialize.ts │ │ ├── instrumentations │ │ │ ├── console │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ └── types.ts │ │ │ ├── errors │ │ │ │ ├── const.ts │ │ │ │ ├── getErrorDetails.test.ts │ │ │ │ ├── getErrorDetails.ts │ │ │ │ ├── getValueAndTypeFromMessage.ts │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ ├── registerOnerror.test.ts │ │ │ │ ├── registerOnerror.ts │ │ │ │ ├── registerOnunhandledrejection.ts │ │ │ │ ├── stackFrames │ │ │ │ │ ├── buildStackFrame.ts │ │ │ │ │ ├── const.ts │ │ │ │ │ ├── getDataFromSafariExtensions.ts │ │ │ │ │ ├── getStackFramesFromError.test.ts │ │ │ │ │ ├── getStackFramesFromError.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── parseStacktrace.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── instrumentationConstants.ts │ │ │ ├── performance │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ ├── navigation.test.ts │ │ │ │ ├── navigation.ts │ │ │ │ ├── performanceConstants.ts │ │ │ │ ├── performanceUtils.test.ts │ │ │ │ ├── performanceUtils.ts │ │ │ │ ├── performanceUtilsTestData.ts │ │ │ │ ├── resource.test.ts │ │ │ │ ├── resource.ts │ │ │ │ └── types.ts │ │ │ ├── session │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ └── sessionManager │ │ │ │ │ ├── PersistentSessionsManager.test.ts │ │ │ │ │ ├── PersistentSessionsManager.ts │ │ │ │ │ ├── VolatileSessionManager.ts │ │ │ │ │ ├── VolatileSessionsManager.test.ts │ │ │ │ │ ├── getSessionManagerByConfig.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sampling.test.ts │ │ │ │ │ ├── sampling.ts │ │ │ │ │ ├── sessionConstants.ts │ │ │ │ │ ├── sessionManagerUtils.test.ts │ │ │ │ │ ├── sessionManagerUtils.ts │ │ │ │ │ └── types.ts │ │ │ ├── userActions │ │ │ │ ├── const.ts │ │ │ │ ├── domMutationMonitor.test.ts │ │ │ │ ├── domMutationMonitor.ts │ │ │ │ ├── httpRequestMonitor.test.ts │ │ │ │ ├── httpRequestMonitor.ts │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ ├── performanceEntriesMonitor.ts │ │ │ │ ├── processUserActionEventHandler.test.ts │ │ │ │ ├── processUserActionEventHandler.ts │ │ │ │ ├── types.ts │ │ │ │ ├── util.test.ts │ │ │ │ └── util.ts │ │ │ ├── view │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ └── instrumentation.ts │ │ │ └── webVitals │ │ │ │ ├── index.ts │ │ │ │ ├── instrumentation.test.ts │ │ │ │ ├── instrumentation.ts │ │ │ │ ├── webVitalsBasic.test.ts │ │ │ │ ├── webVitalsBasic.ts │ │ │ │ ├── webVitalsWithAttribution.test.ts │ │ │ │ └── webVitalsWithAttribution.ts │ │ ├── metas │ │ │ ├── browser │ │ │ │ ├── index.ts │ │ │ │ └── meta.ts │ │ │ ├── index.ts │ │ │ ├── k6 │ │ │ │ ├── index.ts │ │ │ │ └── meta.ts │ │ │ ├── page │ │ │ │ ├── index.ts │ │ │ │ ├── meta.test.ts │ │ │ │ └── meta.ts │ │ │ ├── sdk │ │ │ │ ├── index.ts │ │ │ │ └── meta.ts │ │ │ └── session │ │ │ │ ├── createSession.ts │ │ │ │ └── index.ts │ │ ├── transports │ │ │ ├── console │ │ │ │ ├── index.ts │ │ │ │ ├── transport.ts │ │ │ │ └── types.ts │ │ │ ├── fetch │ │ │ │ ├── index.ts │ │ │ │ ├── transport.test.ts │ │ │ │ ├── transport.ts │ │ │ │ └── types.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ ├── throttle.test.ts │ │ │ ├── throttle.ts │ │ │ ├── url.ts │ │ │ ├── urls.test.ts │ │ │ ├── webStorage.test.ts │ │ │ └── webStorage.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── web-tracing │ ├── .browserslistrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── setup.jest.ts │ ├── src │ ├── faroMetaAttributesSpanProcessor.test.ts │ ├── faroMetaAttributesSpanProcessor.ts │ ├── faroTraceExporter.ts │ ├── faroTraceExporter.utils.test.ts │ ├── faroTraceExporter.utils.ts │ ├── faroUserActionSpanProcessor.test.ts │ ├── faroUserActionSpanProcessor.ts │ ├── faroXhrInstrumentation.ts │ ├── getDefaultOTELInstrumentations.test.ts │ ├── getDefaultOTELInstrumentations.ts │ ├── index.ts │ ├── instrumentation.ts │ ├── instrumentationUtils.test.ts │ ├── instrumentationUtils.ts │ ├── sampler.test.ts │ ├── sampler.ts │ ├── semconv.ts │ ├── sessionSpanProcessor.ts │ └── types.ts │ ├── tsconfig.bundle.json │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── rollup.config.base.js ├── tsconfig.base.cjs.json ├── tsconfig.base.esm.json ├── tsconfig.base.json ├── tsconfig.base.spec.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | 4 | # IDEs 5 | .idea 6 | .vscode 7 | 8 | # App-specifics 9 | .cache 10 | .drone 11 | .eslintcache 12 | .husky 13 | coverage 14 | cypress/videos 15 | cypress/screenshots 16 | demo/logs 17 | demo/stats.html 18 | demo/vite.config.ts.timestamp-*.mjs 19 | dist 20 | docs 21 | infra 22 | node_modules 23 | packages/core/src/version.ts 24 | 25 | # Logs 26 | *.log 27 | lerna-debug.log* 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # Misc 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | # Docker container can't access other hosts by localhost while Docker host can't access containers by their name 2 | DEMO_SERVER_AGENT_HOST=localhost 3 | 4 | DEMO_SERVER_DATABASE_HOST=localhost 5 | 6 | DEMO_CLIENT_PACKAGE_NAMESPACE=demo-local 7 | DEMO_CLIENT_PACKAGE_NAME=@grafana/faro-demo-client 8 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DEMO_SERVER_DATABASE_HOST= 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | 4 | # IDEs 5 | .idea 6 | .vscode 7 | 8 | # App-specifics 9 | .cache 10 | .eslintcache 11 | .husky 12 | coverage 13 | cypress/videos 14 | cypress/screenshots 15 | demo/logs 16 | demo/stats.html 17 | demo/vite.config.ts.timestamp-*.mjs 18 | dist 19 | node_modules 20 | !.lintstagedrc.js 21 | 22 | # Logs 23 | *.log 24 | lerna-debug.log* 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Misc 30 | .DS_Store 31 | 32 | # App plugin 33 | infra/grafana/plugins/ 34 | infra/grafana/plugins-provisioning/*.yaml 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/CHANGE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change request 3 | about: Request a change of an existing feature 4 | title: '' 5 | labels: 'change' 6 | assignees: '' 7 | --- 8 | 9 | ## Description 10 | 11 | 12 | 13 | 14 | 15 | ## Proposed solution 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## Context 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | --- 8 | 9 | ## Description 10 | 11 | 12 | 13 | 14 | 15 | ## Proposed solution 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## Context 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 4 | 5 | ## What 6 | 7 | 8 | 9 | ## Links 10 | 11 | 12 | 13 | ## Checklist 14 | 15 | - [ ] Tests added 16 | - [ ] Changelog updated 17 | - [ ] Documentation updated 18 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'grafana/faro-web-sdk' 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b 16 | with: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Approve a PR 19 | run: gh pr review --approve $PR_URL 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | - name: Enable auto-merge for Dependabot PRs 24 | if: steps.metadata.outputs.update-type == 'version-update:semver-patch' 25 | run: gh pr merge --auto --squash $PR_URL 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Validate Semantic Pull Request' 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | lint-pr-title: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - id: lint-pr-title 14 | uses: grafana/shared-workflows/actions/lint-pr-title@5d7e361bc7e0a183cde8afe9899fb7b596d2659b 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | # This is also used as the default configuration for the Zizmor reusable 2 | # workflow. 3 | 4 | rules: 5 | unpinned-uses: 6 | config: 7 | policies: 8 | actions/*: any # trust GitHub 9 | grafana/*: any # trust Grafana 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .idea 3 | .vscode 4 | *.iml 5 | 6 | # App-specifics 7 | .cache 8 | .eslintcache 9 | .husky/_ 10 | coverage 11 | cypress/videos 12 | cypress/screenshots 13 | demo/logs/*.log 14 | demo/stats.html 15 | demo/vite.config.ts.timestamp-*.mjs 16 | dist 17 | node_modules 18 | packages/core/src/version.ts 19 | 20 | # Logs 21 | *.log 22 | lerna-debug.log* 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Misc 28 | .DS_Store 29 | 30 | # App plugin 31 | infra/grafana/plugins/ 32 | infra/grafana/plugins-provisioning/*.yaml 33 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn build 5 | yarn git-hooks:pre-commit 6 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx,css,scss,md,yaml,yml,json}': ['prettier -w'], 3 | '*.{js,jsx,ts,tsx}': ['eslint', 'madge --circular'], 4 | '*.md': ['markdownlint'], 5 | }; 6 | -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "fileExtensions": ["ts","tsx"], 3 | "detectiveOptions": { 4 | "ts": { 5 | "skipTypeImports": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-duplicate-heading": { 3 | "siblings_only": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": { 3 | "code_block_line_length": 120, 4 | "heading_line_length": 120, 5 | "line_length": 120, 6 | "tables": false 7 | }, 8 | "MD033": { 9 | "allowed_elements": [ 10 | "img", 11 | "p" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | 4 | # IDEs 5 | .idea 6 | .vscode 7 | 8 | # App-specifics 9 | .cache 10 | # Drone sign step formats it in a way that prettier doesn't like 11 | .drone/drone.yml 12 | .eslintcache 13 | .husky 14 | coverage 15 | cypress/videos 16 | cypress/screenshots 17 | demo/logs 18 | demo/stats.html 19 | demo/vite.config.ts.timestamp-*.mjs 20 | dist 21 | # Lerna reformats this as well 22 | lerna.json 23 | node_modules 24 | 25 | # Logs 26 | *.log 27 | lerna-debug.log* 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # Misc 33 | .DS_Store 34 | 35 | # App plugin 36 | infra/grafana/plugins/ 37 | infra/grafana/plugins-provisioning/*.yaml 38 | .yarn/releases/*.js 39 | 40 | /.nx/workspace-data -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | trailingComma: 'es5', 5 | semi: true, 6 | }; 7 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn-path ".yarn/releases/yarn-1.22.19.cjs" 6 | -------------------------------------------------------------------------------- /LICENSING.md: -------------------------------------------------------------------------------- 1 | # Licensing 2 | 3 | License names used in this document are as per [SPDX License List][spdx-licenses]. 4 | 5 | The default license for this project is [Apache v2][license]. 6 | 7 | [spdx-licenses]: https://spdx.org/licenses/ 8 | [license]: ./LICENSE 9 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Bogdan Matei ( / @bfmatei), Domas Lapinskas ( / @domasx2), Marco Schaefer ( / @codecapitano), Kostas Pelelis ( / @kpelelis) and Elliot Kirk ( / @eskirk) are the main/default maintainers. 2 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:5173', 6 | video: false, 7 | requestTimeout: 3000, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/e2e/demo/logs.cy.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionEvent, LogEvent, LogLevel } from '@grafana/faro-core'; 2 | 3 | context('Console logs', () => { 4 | [LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR].forEach((level) => { 5 | it(`will capture ${level} level`, () => { 6 | cy.interceptCollector((body) => { 7 | let item = 8 | level === 'error' 9 | ? body.exceptions?.find( 10 | (item: ExceptionEvent) => item?.value === `console.error: This is a console ${level} message` 11 | ) 12 | : body.logs?.find( 13 | (item: LogEvent) => item?.level === level && item?.message === `This is a console ${level} message` 14 | ); 15 | 16 | return item != null ? 'log' : undefined; 17 | }); 18 | 19 | cy.visit('/features'); 20 | 21 | cy.clickButton(`btn-log-${level}`); 22 | 23 | cy.wait('@log'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | import type { TransportBody } from '@grafana/faro-core'; 2 | 3 | Cypress.Commands.add('interceptCollector', (aliasGenerator) => { 4 | cy.intercept('POST', '**/collect', (req) => { 5 | req.alias = aliasGenerator(req.body as TransportBody); 6 | 7 | req.reply({ 8 | statusCode: 201, 9 | body: {}, 10 | }); 11 | }); 12 | }); 13 | 14 | Cypress.Commands.add('clickButton', (btnId) => { 15 | cy.get(`[data-cy="${btnId}"]`).click({ force: true }); 16 | }); 17 | 18 | declare global { 19 | // cypress uses namespace typing so we have to extend it as well 20 | // eslint-disable-next-line @typescript-eslint/no-namespace 21 | namespace Cypress { 22 | interface Chainable { 23 | clickButton(btnId: string): Chainable; 24 | interceptCollector(aliasGenerator: (body: TransportBody) => string | undefined): Chainable; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "isolatedModules": false, 6 | "noEmit": true, 7 | "paths": { 8 | "@grafana/faro-*": ["../packages/*/dist/cjs/index.js"] 9 | }, 10 | "rootDir": ".", 11 | "types": ["cypress", "node"] 12 | }, 13 | "include": ["./e2e", "./support"], 14 | "exclude": ["**/*.test.ts"], 15 | "references": [{ "path": "../packages/core/tsconfig.cjs.json" }] 16 | } 17 | -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/errorsViewDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/errorsViewDashboard.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/errorsViewExplore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/errorsViewExplore.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/eventsViewExplore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/eventsViewExplore.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/eventsViewExploreTraces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/eventsViewExploreTraces.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/logsViewExplore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/logsViewExplore.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/measurementsViewDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/measurementsViewDashboard.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/measurementsViewExploreLoki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/measurementsViewExploreLoki.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/measurementsViewExploreMimir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/measurementsViewExploreMimir.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/metasViewApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/metasViewApp.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/metasViewExploreSession.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/metasViewExploreSession.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/metasViewExploreUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/metasViewExploreUser.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/tracesViewAppDocumentLoad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/tracesViewAppDocumentLoad.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/tracesViewAppSeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/tracesViewAppSeed.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/tracesViewExploreDocumentLoad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/tracesViewExploreDocumentLoad.png -------------------------------------------------------------------------------- /demo/docs/assets/instrumentations/tracesViewExploreSeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/instrumentations/tracesViewExploreSeed.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/homepage.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/homepageTraces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/homepageTraces.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/homepageWebVitals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/homepageWebVitals.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/registerError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/registerError.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/registerErrorEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/registerErrorEvents.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/registerErrorTraces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/registerErrorTraces.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seed.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedError.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedErrorEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedErrorEvents.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedErrorTraces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedErrorTraces.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedEvents.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedSuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedSuccess.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedSuccessEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedSuccessEvents.png -------------------------------------------------------------------------------- /demo/docs/assets/userJourney/seedSuccessTraces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/docs/assets/userJourney/seedSuccessTraces.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/logs/.gitkeep -------------------------------------------------------------------------------- /demo/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "ts": "node --loader ts-node/esm --experimental-specifier-resolution=node" 4 | }, 5 | "ext": "ts,tsx,json,html", 6 | "ignore": ["*.test.ts", "node_modules/**/node_modules"], 7 | "watch": ["../node_modules", "node_modules", "src/server", "index.html", "vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /demo/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/demo/public/favicon.png -------------------------------------------------------------------------------- /demo/src/client/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from '../router'; 2 | 3 | import { AuthWrapper } from './AuthWrapper'; 4 | 5 | export function App() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/client/App/AuthWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | import { useLazyGetAuthStateQuery } from '../api'; 4 | import { LoadingScreen } from '../components'; 5 | import { useIsomorphicEffect } from '../hooks'; 6 | 7 | export type AuthWrapperProps = { 8 | children: ReactNode; 9 | }; 10 | 11 | export function AuthWrapper({ children }: AuthWrapperProps) { 12 | const [getAuthState, getAuthStateResult] = useLazyGetAuthStateQuery(); 13 | 14 | useIsomorphicEffect(() => { 15 | if (getAuthStateResult.isUninitialized && !getAuthStateResult.isLoading) { 16 | getAuthState(); 17 | } 18 | }, [getAuthState, getAuthStateResult]); 19 | 20 | if (!getAuthStateResult.isUninitialized && getAuthStateResult.isLoading) { 21 | return ; 22 | } 23 | 24 | return <>{children}; 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/client/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from './App'; 2 | -------------------------------------------------------------------------------- /demo/src/client/api/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | articlesAPI, 3 | useGetArticlesQuery, 4 | useGetArticleQuery, 5 | usePostArticleMutation, 6 | usePostArticleCommentMutation, 7 | } from './articles'; 8 | 9 | export { 10 | authAPI, 11 | useLazyGetAuthStateQuery, 12 | useLazyGetLogoutQuery, 13 | usePostLoginMutation, 14 | usePostRegisterMutation, 15 | } from './auth'; 16 | 17 | export { apiMiddleware } from './middleware'; 18 | 19 | export { apiReducers } from './reducers'; 20 | 21 | export { useLazyGetSeedQuery } from './seed'; 22 | -------------------------------------------------------------------------------- /demo/src/client/api/middleware.ts: -------------------------------------------------------------------------------- 1 | import { articlesAPI } from './articles'; 2 | import { authAPI } from './auth'; 3 | import { seedAPI } from './seed'; 4 | 5 | export const apiMiddleware = [authAPI.middleware, articlesAPI.middleware, seedAPI.middleware]; 6 | -------------------------------------------------------------------------------- /demo/src/client/api/reducers.ts: -------------------------------------------------------------------------------- 1 | import { articlesAPI } from './articles'; 2 | import { authAPI } from './auth'; 3 | import { seedAPI } from './seed'; 4 | 5 | export const apiReducers = { 6 | [authAPI.reducerPath]: authAPI.reducer, 7 | [articlesAPI.reducerPath]: articlesAPI.reducer, 8 | [seedAPI.reducerPath]: seedAPI.reducer, 9 | }; 10 | -------------------------------------------------------------------------------- /demo/src/client/api/seed.ts: -------------------------------------------------------------------------------- 1 | import type { SeedGetSuccessPayload } from '../../common'; 2 | import { createApi } from '../utils'; 3 | 4 | import { baseQuery } from './baseQuery'; 5 | 6 | export const seedAPI = createApi({ 7 | reducerPath: 'seedApi', 8 | baseQuery, 9 | tagTypes: ['Seed'], 10 | endpoints: (builder) => ({ 11 | getSeed: builder.query({ 12 | providesTags: ['Seed'], 13 | query: () => ({ 14 | url: '/seed', 15 | method: 'GET', 16 | }), 17 | }), 18 | }), 19 | }); 20 | 21 | export const { useLazyGetSeedQuery } = seedAPI; 22 | -------------------------------------------------------------------------------- /demo/src/client/components/LoadingScreen/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | export function LoadingScreen() { 2 | return <>Loading...; 3 | } 4 | -------------------------------------------------------------------------------- /demo/src/client/components/LoadingScreen/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadingScreen } from './LoadingScreen'; 2 | -------------------------------------------------------------------------------- /demo/src/client/components/Navbar/index.ts: -------------------------------------------------------------------------------- 1 | export { Navbar } from './Navbar'; 2 | export type { NavbarProps } from './Navbar'; 3 | -------------------------------------------------------------------------------- /demo/src/client/components/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import type { ReactNode } from 'react'; 3 | import { Helmet } from 'react-helmet-async'; 4 | 5 | import { faro } from '@grafana/faro-react'; 6 | 7 | export type PageProps = { 8 | children: ReactNode; 9 | title: string; 10 | view: string; 11 | }; 12 | 13 | export function Page({ children, title, view }: PageProps) { 14 | useEffect(() => { 15 | faro?.api?.setView({ name: view }); 16 | }, [view]); 17 | 18 | return ( 19 | <> 20 | 21 | {title} | Demo 22 | 23 | 24 |

{title}

25 | 26 | {children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /demo/src/client/components/Page/index.ts: -------------------------------------------------------------------------------- 1 | export { Page } from './Page'; 2 | export type { PageProps } from './Page'; 3 | -------------------------------------------------------------------------------- /demo/src/client/components/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadingScreen } from './LoadingScreen'; 2 | 3 | export { Navbar } from './Navbar'; 4 | export type { NavbarProps } from './Navbar'; 5 | 6 | export { Page } from './Page'; 7 | export type { PageProps } from './Page'; 8 | -------------------------------------------------------------------------------- /demo/src/client/faro/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeFaro } from './initialize'; 2 | -------------------------------------------------------------------------------- /demo/src/client/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAppDispatch } from './useAppDispatch'; 2 | 3 | export { useAppSelector } from './useAppSelector'; 4 | 5 | export { useIsomorphicEffect } from './useIsomorphicEffect'; 6 | -------------------------------------------------------------------------------- /demo/src/client/hooks/useAppDispatch.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | 3 | import type { AppDispatch } from '../store'; 4 | 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | -------------------------------------------------------------------------------- /demo/src/client/hooks/useAppSelector.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | 4 | import type { RootState } from '../store'; 5 | 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /demo/src/client/hooks/useIsomorphicEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import type { DependencyList } from 'react'; 3 | 4 | export function useIsomorphicEffect(callback: () => void, dependencies: DependencyList): void { 5 | const hasRun = useRef(false); 6 | 7 | useEffect(() => { 8 | if (!hasRun.current) { 9 | hasRun.current = true; 10 | 11 | return callback(); 12 | } 13 | // eslint-disable-next-line react-hooks/exhaustive-deps 14 | }, [hasRun, ...dependencies]); 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/client/index.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap'; 2 | -------------------------------------------------------------------------------- /demo/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { SSRProvider } from 'react-bootstrap'; 3 | import { hydrateRoot } from 'react-dom/client'; 4 | import { HelmetProvider } from 'react-helmet-async'; 5 | import { Provider as ReduxProvider } from 'react-redux'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | import { FaroErrorBoundary } from '@grafana/faro-react'; 9 | 10 | import { App } from './App'; 11 | import { initializeFaro } from './faro'; 12 | import { createStore } from './store'; 13 | 14 | initializeFaro(); 15 | 16 | hydrateRoot( 17 | document.getElementById('app') as HTMLElement, 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /demo/src/client/layouts/GeneralLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { GeneralLayout } from './GeneralLayout'; 2 | -------------------------------------------------------------------------------- /demo/src/client/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { GeneralLayout } from './GeneralLayout'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/About/index.ts: -------------------------------------------------------------------------------- 1 | export { About } from './About'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/ArticleAdd/ArticleAdd.tsx: -------------------------------------------------------------------------------- 1 | import { Page } from '../../components'; 2 | 3 | import { ArticleAddForm } from './ArticleAddForm'; 4 | 5 | export function ArticleAdd() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/client/pages/ArticleAdd/index.ts: -------------------------------------------------------------------------------- 1 | export { ArticleAdd } from './ArticleAdd'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/ArticleView/index.ts: -------------------------------------------------------------------------------- 1 | export { ArticleView } from './ArticleView'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Articles/index.ts: -------------------------------------------------------------------------------- 1 | export { Articles } from './Articles'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Features/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'react-bootstrap'; 2 | 3 | import { withFaroErrorBoundary, withFaroProfiler } from '@grafana/faro-react'; 4 | 5 | export type CounterProps = { 6 | description: string; 7 | title: string; 8 | value: number; 9 | onChange: (value: number) => void; 10 | }; 11 | 12 | export function CounterComponent({ description, title, value, onChange }: CounterProps) { 13 | return ( 14 | <> 15 |

{title}

16 |

{description}

17 |

18 | Counter: {value}{' '} 19 | 22 |

23 | 24 | ); 25 | } 26 | 27 | export const CounterWithErrorBoundary = withFaroErrorBoundary(CounterComponent, { 28 | fallback: <>The content was broken, 29 | }); 30 | 31 | export const CounterWithProfiler = withFaroProfiler(CounterComponent); 32 | -------------------------------------------------------------------------------- /demo/src/client/pages/Features/MetricsMeasurements.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonGroup } from 'react-bootstrap'; 2 | 3 | import { faro } from '@grafana/faro-react'; 4 | 5 | export function MetricsMeasurements() { 6 | const sendCustomMetric = () => { 7 | faro.api.pushMeasurement({ 8 | type: 'custom', 9 | values: { 10 | my_custom_metric: Math.random(), 11 | }, 12 | }); 13 | }; 14 | 15 | return ( 16 | <> 17 |

Metrics Measurements

18 | 19 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/client/pages/Features/index.ts: -------------------------------------------------------------------------------- 1 | export { Features } from './Features'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Page } from '../../components'; 2 | 3 | export function Home() { 4 | return ( 5 | 6 |

Hi

7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/client/pages/Home/index.ts: -------------------------------------------------------------------------------- 1 | export { Home } from './Home'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Login/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Page } from '../../components'; 2 | 3 | import { LoginForm } from './LoginForm'; 4 | 5 | export function Login() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/client/pages/Login/index.ts: -------------------------------------------------------------------------------- 1 | export { Login } from './Login'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Register/Register.tsx: -------------------------------------------------------------------------------- 1 | import { Page } from '../../components'; 2 | 3 | import { RegisterForm } from './RegisterForm'; 4 | 5 | export function Register() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/client/pages/Register/index.ts: -------------------------------------------------------------------------------- 1 | export { Register } from './Register'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/Seed/index.ts: -------------------------------------------------------------------------------- 1 | export { Seed } from './Seed'; 2 | -------------------------------------------------------------------------------- /demo/src/client/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { About } from './About'; 2 | 3 | export { ArticleAdd } from './ArticleAdd'; 4 | 5 | export { Articles } from './Articles'; 6 | 7 | export { ArticleView } from './ArticleView'; 8 | 9 | export { Features } from './Features'; 10 | 11 | export { Home } from './Home'; 12 | 13 | export { Login } from './Login'; 14 | 15 | export { Register } from './Register'; 16 | 17 | export { Seed } from './Seed'; 18 | -------------------------------------------------------------------------------- /demo/src/client/router/guards/LoggedInGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import type { ReactNode } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { useAppSelector } from '../../hooks'; 6 | import { selectIsUserLoggedIn } from '../../store'; 7 | 8 | export type LoggedInGuardProps = { 9 | children: ReactNode; 10 | }; 11 | 12 | export function LoggedInGuard({ children }: LoggedInGuardProps) { 13 | const navigate = useNavigate(); 14 | 15 | const isLoggedIn = useAppSelector(selectIsUserLoggedIn); 16 | 17 | useEffect(() => { 18 | if (!isLoggedIn) { 19 | navigate('/auth/login'); 20 | } 21 | }, [isLoggedIn, navigate]); 22 | 23 | if (!isLoggedIn) { 24 | return null; 25 | } 26 | 27 | return <>{children}; 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/client/router/guards/LoggedOutGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import type { ReactNode } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import { useAppSelector } from '../../hooks'; 6 | import { selectIsUserLoggedIn } from '../../store'; 7 | 8 | export type LoggedOutGuardProps = { 9 | children: ReactNode; 10 | }; 11 | 12 | export function LoggedOutGuard({ children }: LoggedOutGuardProps) { 13 | const navigate = useNavigate(); 14 | 15 | const isLoggedIn = useAppSelector(selectIsUserLoggedIn); 16 | 17 | useEffect(() => { 18 | if (isLoggedIn) { 19 | navigate('/articles'); 20 | } 21 | }, [isLoggedIn, navigate]); 22 | 23 | if (isLoggedIn) { 24 | return null; 25 | } 26 | 27 | return <>{children}; 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/client/router/guards/index.ts: -------------------------------------------------------------------------------- 1 | export { LoggedInGuard } from './LoggedInGuard'; 2 | export type { LoggedInGuardProps } from './LoggedInGuard'; 3 | 4 | export { LoggedOutGuard } from './LoggedOutGuard'; 5 | export type { LoggedOutGuardProps } from './LoggedOutGuard'; 6 | -------------------------------------------------------------------------------- /demo/src/client/router/index.ts: -------------------------------------------------------------------------------- 1 | export { Router } from './Router'; 2 | -------------------------------------------------------------------------------- /demo/src/client/store/index.ts: -------------------------------------------------------------------------------- 1 | export { createStore } from './store'; 2 | export type { AppDispatch, RootState } from './store'; 3 | 4 | export { 5 | selectIsUserLoggedIn, 6 | selectRootSpanId, 7 | selectRootTraceId, 8 | selectSession, 9 | selectUserData, 10 | setSession, 11 | setUser, 12 | } from './slices'; 13 | export type { FaroState, UserState } from './slices'; 14 | -------------------------------------------------------------------------------- /demo/src/client/store/slices/faro.ts: -------------------------------------------------------------------------------- 1 | import type { PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | import type { MetaSession } from '@grafana/faro-react'; 4 | 5 | import { createSlice } from '../../utils'; 6 | import type { RootState } from '../store'; 7 | 8 | export type FaroState = { 9 | rootSpanId: string | null; 10 | rootTraceId: string | null; 11 | session: MetaSession | null; 12 | }; 13 | 14 | export const initialState: FaroState = { 15 | rootSpanId: null, 16 | rootTraceId: null, 17 | session: null, 18 | }; 19 | 20 | export const faroSlice = createSlice({ 21 | name: 'faro', 22 | initialState, 23 | reducers: { 24 | setSession: (state, action: PayloadAction) => { 25 | state.session = action.payload; 26 | }, 27 | }, 28 | }); 29 | 30 | export const { setSession } = faroSlice.actions; 31 | 32 | export const selectSession = (state: RootState) => state.faro.session; 33 | export const selectRootSpanId = (state: RootState) => state.faro.rootSpanId; 34 | export const selectRootTraceId = (state: RootState) => state.faro.rootTraceId; 35 | -------------------------------------------------------------------------------- /demo/src/client/store/slices/index.ts: -------------------------------------------------------------------------------- 1 | export { faroSlice, selectRootSpanId, selectRootTraceId, selectSession, setSession } from './faro'; 2 | export type { FaroState } from './faro'; 3 | 4 | export { selectIsUserLoggedIn, selectUserData, setUser, userSlice } from './user'; 5 | export type { UserState } from './user'; 6 | -------------------------------------------------------------------------------- /demo/src/client/store/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { apiMiddleware, apiReducers } from '../api'; 4 | import { configureStore, setupListeners } from '../utils'; 5 | 6 | import { faroSlice, userSlice } from './slices'; 7 | 8 | export function createStore(preloadedState: {}) { 9 | const store = configureStore({ 10 | preloadedState, 11 | reducer: combineReducers({ 12 | faro: faroSlice.reducer, 13 | user: userSlice.reducer, 14 | ...apiReducers, 15 | }), 16 | middleware: (getDefaultMiddleware) => 17 | getDefaultMiddleware({ 18 | serializableCheck: false, 19 | }).concat(apiMiddleware), 20 | }); 21 | 22 | setupListeners(store.dispatch); 23 | 24 | return store; 25 | } 26 | 27 | export type RootState = ReturnType['getState']>; 28 | 29 | export type AppDispatch = ReturnType['dispatch']; 30 | -------------------------------------------------------------------------------- /demo/src/client/utils/env.ts: -------------------------------------------------------------------------------- 1 | import type { PublicEnv } from '../../common'; 2 | 3 | export const env: PublicEnv = typeof window !== 'undefined' ? (window as any).__APP_ENV__ : process.env['__APP_ENV__']; 4 | -------------------------------------------------------------------------------- /demo/src/client/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | const formatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'long' }); 2 | 3 | export function formatDate(input: number): string { 4 | return formatter.format(input); 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { env } from './env'; 2 | 3 | export { formatDate } from './formatDate'; 4 | 5 | export { configureStore, createApi, createSlice, fetchBaseQuery, setupListeners } from './reduxToolkit'; 6 | -------------------------------------------------------------------------------- /demo/src/client/utils/reduxToolkit.ts: -------------------------------------------------------------------------------- 1 | import * as toolkitRaw from '@reduxjs/toolkit'; 2 | import * as toolkitQueryReact from '@reduxjs/toolkit/query/react'; 3 | 4 | // RTK Query has some issues during build process 5 | // This is the easiest way to avoid them 6 | 7 | export const { configureStore, createSlice } = ((toolkitRaw as any).default ?? toolkitRaw) as typeof toolkitRaw; 8 | 9 | export const { createApi, fetchBaseQuery, setupListeners } = ((toolkitQueryReact as any).default ?? 10 | toolkitQueryReact) as typeof toolkitQueryReact; 11 | -------------------------------------------------------------------------------- /demo/src/common/types/api/auth.ts: -------------------------------------------------------------------------------- 1 | import type { UserInputModel, UserPublicModel } from '../models'; 2 | 3 | import type { ErrorResponse, SuccessResponse } from './generic'; 4 | 5 | export type AuthRegisterPayload = UserInputModel; 6 | 7 | export type AuthRegisterSuccessPayload = SuccessResponse; 8 | 9 | export type AuthRegisterErrorPayload = ErrorResponse; 10 | 11 | export type AuthLoginPayload = { 12 | email: string; 13 | password: string; 14 | }; 15 | 16 | export type AuthLoginSuccessPayload = SuccessResponse; 17 | 18 | export type AuthLoginErrorPayload = ErrorResponse; 19 | 20 | export type AuthLogoutPayload = {}; 21 | 22 | export type AuthLogoutSuccessPayload = SuccessResponse; 23 | 24 | export type AuthLogoutErrorPayload = ErrorResponse; 25 | 26 | export type AuthGetAuthStatePayload = {}; 27 | 28 | export type AuthGetAuthStateSuccessPayload = SuccessResponse; 29 | 30 | export type AuthGetAuthStateErrorPayload = ErrorResponse; 31 | -------------------------------------------------------------------------------- /demo/src/common/types/api/generic.ts: -------------------------------------------------------------------------------- 1 | export type ErrorResponse = { 2 | success: false; 3 | data: { 4 | message: string; 5 | field?: string; 6 | [label: string]: string | number | boolean | undefined; 7 | }; 8 | spanId: string | null; 9 | traceId: string | null; 10 | }; 11 | 12 | export type SuccessResponse = { 13 | success: true; 14 | data: D; 15 | spanId: string | null; 16 | traceId: string | null; 17 | }; 18 | -------------------------------------------------------------------------------- /demo/src/common/types/api/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ArticleAddErrorPayload, 3 | ArticleAddPayload, 4 | ArticleAddSuccessPayload, 5 | ArticleCommentAddErrorPayload, 6 | ArticleCommentAddPayload, 7 | ArticleCommentAddSuccessPayload, 8 | ArticleGetErrorPayload, 9 | ArticleGetPayload, 10 | ArticleGetSuccessPayload, 11 | ArticlesGetErrorPayload, 12 | ArticlesGetPayload, 13 | ArticlesGetSuccessPayload, 14 | } from './article'; 15 | 16 | export type { 17 | AuthGetAuthStateErrorPayload, 18 | AuthGetAuthStatePayload, 19 | AuthGetAuthStateSuccessPayload, 20 | AuthLoginErrorPayload, 21 | AuthLoginPayload, 22 | AuthLoginSuccessPayload, 23 | AuthLogoutErrorPayload, 24 | AuthLogoutPayload, 25 | AuthLogoutSuccessPayload, 26 | AuthRegisterErrorPayload, 27 | AuthRegisterPayload, 28 | AuthRegisterSuccessPayload, 29 | } from './auth'; 30 | 31 | export type { SeedGetErrorPayload, SeedGetPayload, SeedGetSuccessPayload } from './seed'; 32 | -------------------------------------------------------------------------------- /demo/src/common/types/api/seed.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorResponse, SuccessResponse } from './generic'; 2 | 3 | export type SeedGetPayload = {}; 4 | 5 | export type SeedGetSuccessPayload = SuccessResponse; 6 | 7 | export type SeedGetErrorPayload = ErrorResponse; 8 | -------------------------------------------------------------------------------- /demo/src/common/types/models/article.ts: -------------------------------------------------------------------------------- 1 | import type { CommentPublicModel } from './comment'; 2 | import type { UserModel, UserPublicModel } from './user'; 3 | 4 | export type ArticleModel = { 5 | date: number; 6 | id: string; 7 | name: string; 8 | text: string; 9 | userId: UserModel['id']; 10 | }; 11 | 12 | export type ArticleInputModel = { 13 | name: string; 14 | text: string; 15 | }; 16 | 17 | export type ArticlePublicModel = { 18 | comments: CommentPublicModel[]; 19 | date: number; 20 | id: string; 21 | name: string; 22 | text: string; 23 | user: UserPublicModel; 24 | }; 25 | -------------------------------------------------------------------------------- /demo/src/common/types/models/comment.ts: -------------------------------------------------------------------------------- 1 | import type { ArticleModel } from './article'; 2 | import type { UserModel, UserPublicModel } from './user'; 3 | 4 | export type CommentModel = { 5 | articleId: ArticleModel['id']; 6 | date: number; 7 | id: string; 8 | text: string; 9 | userId: UserModel['id']; 10 | }; 11 | 12 | export type CommentInputModel = { 13 | text: string; 14 | }; 15 | 16 | export type CommentPublicModel = { 17 | date: number; 18 | id: string; 19 | text: string; 20 | user: UserPublicModel; 21 | }; 22 | -------------------------------------------------------------------------------- /demo/src/common/types/models/index.ts: -------------------------------------------------------------------------------- 1 | export type { ArticleModel, ArticleInputModel, ArticlePublicModel } from './article'; 2 | 3 | export type { CommentModel, CommentInputModel, CommentPublicModel } from './comment'; 4 | 5 | export type { UserModel, UserInputModel, UserPublicModel } from './user'; 6 | -------------------------------------------------------------------------------- /demo/src/common/types/models/user.ts: -------------------------------------------------------------------------------- 1 | export type UserModel = { 2 | email: string; 3 | id: string; 4 | name: string; 5 | password: string; 6 | }; 7 | 8 | export type UserPublicModel = { 9 | email: string; 10 | id: string; 11 | name: string; 12 | }; 13 | 14 | export type UserInputModel = { 15 | email: string; 16 | name: string; 17 | password: string; 18 | }; 19 | -------------------------------------------------------------------------------- /demo/src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { getEnvConfig, getPublicEnvConfig } from './env'; 2 | export type { Env, PublicEnv } from './env'; 3 | -------------------------------------------------------------------------------- /demo/src/server/app/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeApp } from './initialize'; 2 | -------------------------------------------------------------------------------- /demo/src/server/app/initialize.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import cookieParser from 'cookie-parser'; 3 | import express from 'express'; 4 | import type { Express } from 'express'; 5 | 6 | import { logger } from '../logger'; 7 | import { registerRoutes } from '../routes'; 8 | import { env } from '../utils'; 9 | 10 | export async function initializeApp(): Promise { 11 | const app = express(); 12 | 13 | if (env.mode.prod) { 14 | app.use((await import('compression')).default()); 15 | } 16 | 17 | app.use(bodyParser.json()); 18 | app.use(cookieParser()); 19 | 20 | await registerRoutes(app); 21 | 22 | if (!env.mode.test) { 23 | try { 24 | await app.listen(Number(env.server.port), '0.0.0.0'); 25 | 26 | logger.info(`App is running at: http://localhost:${env.server.port}`); 27 | logger.info(`Grafana is running at: http://localhost:${env.grafana.port}`); 28 | } catch (err) { 29 | logger.error(err); 30 | } 31 | } 32 | 33 | return app; 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/server/db/db.ts: -------------------------------------------------------------------------------- 1 | import type { Sequelize } from 'sequelize'; 2 | 3 | export let db: Sequelize; 4 | 5 | export function setDb(newDb: Sequelize): void { 6 | db = newDb; 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/server/db/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | addArticle, 3 | getArticleById, 4 | getArticlePublicFromArticle, 5 | getArticlesPublicFromArticles, 6 | getArticlesLength, 7 | getArticlesByPage, 8 | } from './articles'; 9 | 10 | export { 11 | addComment, 12 | getCommentById, 13 | getCommentPublicFromComment, 14 | getCommentsPublicFromComments, 15 | getCommentsByArticleId, 16 | } from './comments'; 17 | 18 | export { addUser, getUserByEmail, getUserById, getUserPublicFromUser, getUsersPublicFromUsers } from './users'; 19 | -------------------------------------------------------------------------------- /demo/src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | addArticle, 3 | addComment, 4 | addUser, 5 | getArticleById, 6 | getArticlePublicFromArticle, 7 | getArticlesPublicFromArticles, 8 | getArticlesLength, 9 | getArticlesByPage, 10 | getCommentById, 11 | getCommentPublicFromComment, 12 | getCommentsPublicFromComments, 13 | getCommentsByArticleId, 14 | getUserByEmail, 15 | getUserById, 16 | getUserPublicFromUser, 17 | getUsersPublicFromUsers, 18 | } from './handlers'; 19 | 20 | export { initializeDb } from './initialize'; 21 | 22 | export { mocks } from './mock'; 23 | -------------------------------------------------------------------------------- /demo/src/server/db/initialize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | 3 | import { logger } from '../logger/logger'; 4 | import { env } from '../utils'; 5 | 6 | import { setDb } from './db'; 7 | import { initializeArticle, initializeComment, initializeUser } from './repositories'; 8 | 9 | export async function initializeDb(): Promise { 10 | const db = new Sequelize({ 11 | database: env.database.name, 12 | host: env.database.host, 13 | password: env.database.password, 14 | port: Number(env.database.port), 15 | username: env.database.user, 16 | dialect: 'postgres', 17 | }); 18 | 19 | try { 20 | await db.authenticate(); 21 | 22 | await initializeUser(db); 23 | await initializeArticle(db); 24 | await initializeComment(db); 25 | 26 | setDb(db); 27 | 28 | logger.info('Database connection has been established successfully.'); 29 | } catch (err) { 30 | logger.error(err); 31 | 32 | throw err; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/server/db/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export { Article, initializeArticle } from './article'; 2 | export type { ArticleShape } from './article'; 3 | 4 | export { Comment, initializeComment } from './comment'; 5 | export type { CommentShape } from './comment'; 6 | 7 | export { initializeUser, User } from './user'; 8 | export type { UserShape } from './user'; 9 | -------------------------------------------------------------------------------- /demo/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | 3 | import './otel/initialize'; 4 | 5 | import { initializeApp } from './app'; 6 | import { initializeDb } from './db'; 7 | import { initializeLogger } from './logger'; 8 | import { initializeMetrics } from './metrics'; 9 | 10 | initializeLogger(); 11 | 12 | initializeMetrics(); 13 | 14 | initializeDb().then(async () => { 15 | await initializeApp(); 16 | }); 17 | -------------------------------------------------------------------------------- /demo/src/server/logger/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeLogger } from './initialize'; 2 | 3 | export { logger } from './logger'; 4 | -------------------------------------------------------------------------------- /demo/src/server/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from 'winston'; 2 | 3 | export let logger: Logger; 4 | 5 | export function setLogger(newLogger: Logger): void { 6 | logger = newLogger; 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/server/metrics/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeMetrics } from './initialize'; 2 | -------------------------------------------------------------------------------- /demo/src/server/metrics/initialize.ts: -------------------------------------------------------------------------------- 1 | import { collectDefaultMetrics } from 'prom-client'; 2 | 3 | export function initializeMetrics(): void { 4 | collectDefaultMetrics(); 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/server/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import { removeAuthorizationToken, sendUnauthenticatedError, verifyToken } from '../utils'; 3 | import type { RequestHandler } from '../utils'; 4 | 5 | export const authMiddleware: RequestHandler = async (_req, res, next) => { 6 | try { 7 | const user = await verifyToken(res.locals.token); 8 | 9 | if (!user) { 10 | removeAuthorizationToken(res); 11 | 12 | return sendUnauthenticatedError(res); 13 | } 14 | 15 | res.locals.user = user; 16 | 17 | next(); 18 | } catch (err) { 19 | logger.error(err); 20 | 21 | removeAuthorizationToken(res); 22 | 23 | sendUnauthenticatedError(res); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /demo/src/server/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { authMiddleware } from './auth'; 2 | 3 | export { tokenMiddleware } from './token'; 4 | 5 | export { traceparentMiddleware } from './traceparent'; 6 | 7 | export { serverTimingMiddleware } from './serverTiming'; 8 | -------------------------------------------------------------------------------- /demo/src/server/middlewares/serverTiming.ts: -------------------------------------------------------------------------------- 1 | import { trace } from '@opentelemetry/api'; 2 | 3 | import type { RequestHandler } from '../utils'; 4 | 5 | export const serverTimingMiddleware: RequestHandler = async (_req, res, next) => { 6 | const span = trace.getActiveSpan(); 7 | 8 | if (span != null) { 9 | const { traceId, spanId, traceFlags } = span.spanContext(); 10 | const w3cTraceparent = `traceparent;desc="00-${traceId}-${spanId}-${traceFlags}"`; 11 | 12 | res.setHeader('Server-Timing', w3cTraceparent); 13 | } 14 | 15 | next(); 16 | }; 17 | -------------------------------------------------------------------------------- /demo/src/server/middlewares/token.ts: -------------------------------------------------------------------------------- 1 | import { authorizationCookieName } from '../utils'; 2 | import type { RequestHandler } from '../utils'; 3 | 4 | export const tokenMiddleware: RequestHandler = async (req, res, next) => { 5 | res.locals.token = req.cookies[authorizationCookieName]?.split(' ')[1]; 6 | 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /demo/src/server/middlewares/traceparent.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '../utils'; 2 | 3 | export const traceparentMiddleware: RequestHandler = async (req, res, next) => { 4 | const { traceparent } = req.headers; 5 | const [, traceId, spanId] = ((traceparent ?? '') as string).split('-'); 6 | 7 | res.locals.requestTraceId = traceId ?? null; 8 | res.locals.requestSpanId = spanId ?? null; 9 | 10 | next(); 11 | }; 12 | -------------------------------------------------------------------------------- /demo/src/server/otel/index.ts: -------------------------------------------------------------------------------- 1 | export { getActiveSpan, getActiveSpanContext } from './span'; 2 | 3 | export { getTracer } from './tracer'; 4 | -------------------------------------------------------------------------------- /demo/src/server/otel/span.ts: -------------------------------------------------------------------------------- 1 | import { trace } from '@opentelemetry/api'; 2 | import type { Span, SpanContext } from '@opentelemetry/api'; 3 | 4 | export function getActiveSpan(): Span | undefined { 5 | return trace.getActiveSpan(); 6 | } 7 | 8 | export function getActiveSpanContext(): SpanContext | undefined { 9 | return getActiveSpan()?.spanContext(); 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/server/otel/tracer.ts: -------------------------------------------------------------------------------- 1 | import { trace } from '@opentelemetry/api'; 2 | import type { Tracer } from '@opentelemetry/api'; 3 | 4 | import { env } from '../utils'; 5 | 6 | export function getTracer(): Tracer { 7 | return trace.getTracer(env.server.packageName, env.package.version); 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/articles/getArticleHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ArticleGetPayload, ArticleGetSuccessPayload } from '../../../../common'; 2 | import { getArticleById, getArticlePublicFromArticle } from '../../../db'; 3 | import { logger } from '../../../logger'; 4 | import { sendError, sendSuccess } from '../../../utils'; 5 | import type { RequestHandler } from '../../../utils'; 6 | 7 | export const getArticleHandler: RequestHandler = async ( 8 | req, 9 | res 10 | ) => { 11 | try { 12 | const { id } = req.params; 13 | 14 | if (!id) { 15 | return sendError(res, 'Article not found', 404); 16 | } 17 | 18 | const articleRaw = await getArticleById(id); 19 | 20 | if (!articleRaw) { 21 | return sendError(res, 'Article not found', 404); 22 | } 23 | 24 | const article = await getArticlePublicFromArticle(articleRaw); 25 | 26 | sendSuccess(res, article); 27 | } catch (err) { 28 | logger.error(err); 29 | 30 | sendError(res, err); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/articles/getArticlesHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ArticlesGetPayload, ArticlesGetSuccessPayload } from '../../../../common'; 2 | import { getArticlesByPage, getArticlesLength, getArticlesPublicFromArticles } from '../../../db'; 3 | import { logger } from '../../../logger'; 4 | import { sendError, sendSuccess } from '../../../utils'; 5 | import type { RequestHandler } from '../../../utils'; 6 | 7 | export const getArticlesHandler: RequestHandler<{}, ArticlesGetSuccessPayload, {}, ArticlesGetPayload> = async ( 8 | req, 9 | res 10 | ) => { 11 | try { 12 | const { page } = req.query; 13 | 14 | const pageParam = !page ? 0 : parseInt(req.query['page'], 10); 15 | 16 | const articlesRaw = await getArticlesByPage(pageParam); 17 | const articles = await getArticlesPublicFromArticles(articlesRaw); 18 | 19 | const totalSize = await getArticlesLength(); 20 | 21 | sendSuccess(res, { 22 | items: articles, 23 | totalSize, 24 | }); 25 | } catch (err) { 26 | logger.error(err); 27 | 28 | sendError(res, err); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/articles/index.ts: -------------------------------------------------------------------------------- 1 | export { registerApiArticlesRoutes } from './registerApiArticlesRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/articles/registerApiArticlesRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import type { Express } from 'express'; 3 | 4 | import { authMiddleware } from '../../../middlewares'; 5 | 6 | import { addArticleCommentHandler } from './addArticleCommentHandler'; 7 | import { addArticleHandler } from './addArticleHandler'; 8 | import { getArticleHandler } from './getArticleHandler'; 9 | import { getArticlesHandler } from './getArticlesHandler'; 10 | 11 | export function registerApiArticlesRoutes(apiRouter: Router, _app: Express): void { 12 | const articlesRouter = Router(); 13 | 14 | articlesRouter.get('/', getArticlesHandler); 15 | articlesRouter.get('/:id', getArticleHandler); 16 | articlesRouter.post('/', addArticleHandler); 17 | articlesRouter.post('/:id/comment', addArticleCommentHandler); 18 | 19 | apiRouter.use('/articles', authMiddleware, articlesRouter); 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { registerApiAuthRoutes } from './registerApiAuthRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/auth/logoutHandler.ts: -------------------------------------------------------------------------------- 1 | import type { AuthLogoutPayload, AuthLogoutSuccessPayload } from '../../../../common'; 2 | import { logger } from '../../../logger'; 3 | import { removeAuthorizationToken, sendError, sendSuccess } from '../../../utils'; 4 | import type { RequestHandler } from '../../../utils'; 5 | 6 | export const logoutHandler: RequestHandler<{}, AuthLogoutSuccessPayload, AuthLogoutPayload, {}> = (_req, res) => { 7 | try { 8 | removeAuthorizationToken(res); 9 | 10 | sendSuccess(res, true); 11 | } catch (err) { 12 | logger.error(err); 13 | 14 | sendError(res, err); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/auth/registerApiAuthRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import type { Express } from 'express'; 3 | 4 | import { authMiddleware } from '../../../middlewares'; 5 | 6 | import { loginHandler } from './loginHandler'; 7 | import { logoutHandler } from './logoutHandler'; 8 | import { registerHandler } from './registerHandler'; 9 | import { stateHandler } from './stateHandler'; 10 | 11 | export function registerApiAuthRoutes(apiRouter: Router, _app: Express): void { 12 | const authRouter = Router(); 13 | 14 | authRouter.post('/register', registerHandler); 15 | authRouter.post('/login', loginHandler); 16 | authRouter.get('/logout', authMiddleware, logoutHandler); 17 | authRouter.get('/state', authMiddleware, stateHandler); 18 | 19 | apiRouter.use('/auth', authRouter); 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/auth/stateHandler.ts: -------------------------------------------------------------------------------- 1 | import type { AuthGetAuthStatePayload, AuthGetAuthStateSuccessPayload } from '../../../../common'; 2 | import { logger } from '../../../logger'; 3 | import { sendError, sendSuccess } from '../../../utils'; 4 | import type { RequestHandler } from '../../../utils'; 5 | 6 | export const stateHandler: RequestHandler<{}, AuthGetAuthStateSuccessPayload, AuthGetAuthStatePayload, {}> = async ( 7 | _req, 8 | res 9 | ) => { 10 | try { 11 | sendSuccess(res, res.locals.user, 201); 12 | } catch (err) { 13 | logger.error(err); 14 | 15 | sendError(res, err); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/index.ts: -------------------------------------------------------------------------------- 1 | export { registerApiRoutes } from './registerApiRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/registerApiRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import type { Express } from 'express'; 3 | 4 | import { registerApiArticlesRoutes } from './articles'; 5 | import { registerApiAuthRoutes } from './auth'; 6 | import { registerApiSeedRoutes } from './seed'; 7 | 8 | export function registerApiRoutes(globalRouter: Router, app: Express): void { 9 | const apiRouter = Router(); 10 | 11 | registerApiAuthRoutes(apiRouter, app); 12 | registerApiArticlesRoutes(apiRouter, app); 13 | registerApiSeedRoutes(apiRouter, app); 14 | 15 | globalRouter.use('/api', apiRouter); 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/seed/index.ts: -------------------------------------------------------------------------------- 1 | export { registerApiSeedRoutes } from './registerApiSeedRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/api/seed/registerApiSeedRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import type { Express } from 'express'; 3 | 4 | import { seedHandler } from './seedHandler'; 5 | 6 | export function registerApiSeedRoutes(apiRouter: Router, _app: Express): void { 7 | const seedRouter = Router(); 8 | 9 | seedRouter.get('/', seedHandler); 10 | 11 | apiRouter.use('/seed', seedRouter); 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | export { registerRoutes } from './registerRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/metrics/getMetricsHandler.ts: -------------------------------------------------------------------------------- 1 | import { register } from 'prom-client'; 2 | 3 | import type { RequestHandler } from '../../utils'; 4 | 5 | export const getMetricsHandler: RequestHandler<{}, any, any, {}> = async (_req, res) => { 6 | res.setHeader('Content-Type', register.contentType); 7 | res.end(await register.metrics()); 8 | }; 9 | -------------------------------------------------------------------------------- /demo/src/server/routes/metrics/index.ts: -------------------------------------------------------------------------------- 1 | export { registerMetricsRoutes } from './registerMetricsRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/metrics/registerMetricsRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import type { Express } from 'express'; 3 | 4 | import { getMetricsHandler } from './getMetricsHandler'; 5 | 6 | export function registerMetricsRoutes(globalRouter: Router, _app: Express): void { 7 | const metricsRouter = Router(); 8 | 9 | metricsRouter.get('/', getMetricsHandler); 10 | 11 | globalRouter.use('/metrics', metricsRouter); 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/server/routes/registerRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Express, Router } from 'express'; 2 | 3 | import { serverTimingMiddleware, tokenMiddleware, traceparentMiddleware } from '../middlewares'; 4 | 5 | import { registerApiRoutes } from './api'; 6 | import { registerMetricsRoutes } from './metrics'; 7 | import { registerRenderRoutes } from './render'; 8 | 9 | export async function registerRoutes(app: Express): Promise { 10 | const globalRouter = Router(); 11 | 12 | app.use(serverTimingMiddleware); 13 | 14 | app.use(tokenMiddleware); 15 | 16 | app.use(traceparentMiddleware); 17 | 18 | registerApiRoutes(globalRouter, app); 19 | 20 | registerMetricsRoutes(globalRouter, app); 21 | 22 | await registerRenderRoutes(globalRouter, app); 23 | 24 | app.use(globalRouter); 25 | 26 | return globalRouter; 27 | } 28 | -------------------------------------------------------------------------------- /demo/src/server/routes/render/index.ts: -------------------------------------------------------------------------------- 1 | export { registerRenderRoutes } from './registerRenderRoutes'; 2 | -------------------------------------------------------------------------------- /demo/src/server/routes/render/registerRenderRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { Express, Router } from 'express'; 2 | 3 | import { env } from '../../utils'; 4 | 5 | export async function registerRenderRoutes(globalRouter: Router, app: Express): Promise { 6 | if (env.mode.prod) { 7 | const registerRenderProdRoutes = (await import('./renderProd')).registerRenderProdRoutes; 8 | await registerRenderProdRoutes(globalRouter, app); 9 | } else { 10 | const registerRenderDevRoutes = (await import('./renderDev')).registerRenderDevRoutes; 11 | await registerRenderDevRoutes(globalRouter, app); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/server/routes/render/renderProd.ts: -------------------------------------------------------------------------------- 1 | import type { Express, Router } from 'express'; 2 | import { readFileSync } from 'node:fs'; 3 | 4 | import { logger } from '../../logger'; 5 | import { sendError, toAbsolutePath } from '../../utils'; 6 | import type { Request, Response } from '../../utils'; 7 | 8 | import { renderPage } from './renderPage'; 9 | 10 | export async function registerRenderProdRoutes(globalRouter: Router, _app: Express): Promise { 11 | globalRouter.use( 12 | (await import('serve-static')).default(toAbsolutePath('dist/client'), { 13 | index: false, 14 | }) 15 | ); 16 | 17 | globalRouter.use('*splat', async (req: Request, res: Response) => { 18 | try { 19 | const template = readFileSync(toAbsolutePath('dist/client/index.html'), 'utf-8'); 20 | 21 | const render = (await import('./renderToString'))['renderToString']; 22 | 23 | await renderPage(req, res, template, render); 24 | } catch (err) { 25 | logger.error(err); 26 | 27 | sendError(res, err); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/server/utils/const.ts: -------------------------------------------------------------------------------- 1 | export const authorizationCookieName = 'authorization'; 2 | export const authorizationSecret = 'mySuperSecretToken'; 3 | -------------------------------------------------------------------------------- /demo/src/server/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { resolve } from 'node:path'; 3 | 4 | import { getEnvConfig, getPublicEnvConfig } from '../../common'; 5 | 6 | config({ 7 | path: resolve(process.cwd(), '../.env'), 8 | }); 9 | 10 | config({ 11 | path: resolve(process.cwd(), '../.env.local'), 12 | override: true, 13 | }); 14 | 15 | if (process.env['IS_TEST']) { 16 | config({ 17 | path: resolve(process.cwd(), '../.env.test'), 18 | override: true, 19 | }); 20 | } 21 | 22 | export const env = getEnvConfig(process.env, process.env['NODE_ENV']); 23 | 24 | process.env['__APP_ENV__'] = JSON.stringify(getPublicEnvConfig(env)); 25 | -------------------------------------------------------------------------------- /demo/src/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { authorizationCookieName, authorizationSecret } from './const'; 2 | 3 | export { env } from './env'; 4 | 5 | export { removeAuthorizationToken } from './removeAuthorizationToken'; 6 | 7 | export { sendError } from './sendError'; 8 | 9 | export { sendFormValidationError } from './sendFormValidationError'; 10 | 11 | export { sendSuccess } from './sendSuccess'; 12 | 13 | export { sendUnauthenticatedError } from './sendUnauthenticatedError'; 14 | 15 | export { setAuthorizationToken } from './setAuthorizationToken'; 16 | 17 | export { signToken } from './signToken'; 18 | 19 | export { toAbsolutePath } from './toAbsolutePath'; 20 | 21 | export type { MiddlewareLocals, Request, RequestHandler, Response } from './types'; 22 | 23 | export { verifyToken } from './verifyToken'; 24 | -------------------------------------------------------------------------------- /demo/src/server/utils/removeAuthorizationToken.ts: -------------------------------------------------------------------------------- 1 | import { authorizationCookieName } from './const'; 2 | import type { Response } from './types'; 3 | 4 | export function removeAuthorizationToken(res: Response): void { 5 | res.cookie(authorizationCookieName, 'expired', { 6 | httpOnly: true, 7 | maxAge: -1, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/server/utils/sendError.ts: -------------------------------------------------------------------------------- 1 | import { isInstanceOf } from '@grafana/faro-core'; 2 | 3 | import type { Response } from './types'; 4 | 5 | export function sendError(res: Response, message: Error | string, statusCode = 500, additionalProperties = {}): void { 6 | const actualMessage = isInstanceOf(message, Error) ? (message as Error).message : message; 7 | 8 | res.status(statusCode).send({ 9 | success: false, 10 | data: { 11 | message: actualMessage, 12 | ...additionalProperties, 13 | }, 14 | spanId: res.locals.requestSpanId, 15 | traceId: res.locals.requestTraceId, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/server/utils/sendFormValidationError.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from './types'; 2 | 3 | export function sendFormValidationError(res: Response, field: string, message: string, statusCode = 400): void { 4 | res.status(statusCode).send({ 5 | success: false, 6 | data: { 7 | field, 8 | message, 9 | }, 10 | spanId: res.locals.requestSpanId, 11 | traceId: res.locals.requestTraceId, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/server/utils/sendSuccess.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from './types'; 2 | 3 | export function sendSuccess(res: Response, data: {} | any[] | boolean, statusCode = 200): void { 4 | res.status(statusCode).send({ 5 | data, 6 | spanId: res.locals.requestSpanId, 7 | success: true, 8 | traceId: res.locals.requestTraceId, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/server/utils/sendUnauthenticatedError.ts: -------------------------------------------------------------------------------- 1 | import { sendError } from './sendError'; 2 | import type { Response } from './types'; 3 | 4 | export function sendUnauthenticatedError(res: Response): void { 5 | sendError(res, 'Unauthenticated', 401); 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/server/utils/setAuthorizationToken.ts: -------------------------------------------------------------------------------- 1 | import { authorizationCookieName } from './const'; 2 | import type { Response } from './types'; 3 | 4 | export function setAuthorizationToken(res: Response, token: string): void { 5 | res.cookie(authorizationCookieName, `Bearer ${token}`, { 6 | httpOnly: true, 7 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24), 8 | encode: (value) => value, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/server/utils/signToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | import type { UserPublicModel } from '../../common'; 4 | 5 | import { authorizationSecret } from './const'; 6 | 7 | export function signToken(userPublic: UserPublicModel): string { 8 | return jwt.sign(userPublic, authorizationSecret, { 9 | algorithm: 'HS256', 10 | expiresIn: '1d', 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/server/utils/toAbsolutePath.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | export function toAbsolutePath(path: string): string { 4 | return resolve('./', path); 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/server/utils/verifyToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | import type { UserPublicModel } from '../../common'; 4 | import { getUserById, getUserPublicFromUser } from '../db/handlers/users'; 5 | import { logger } from '../logger/logger'; 6 | 7 | import { authorizationSecret } from './const'; 8 | 9 | export async function verifyToken(token: string | undefined): Promise { 10 | try { 11 | if (!token) { 12 | return undefined; 13 | } 14 | 15 | jwt.verify(token, authorizationSecret, { 16 | algorithms: ['HS256'], 17 | }); 18 | 19 | const decodedToken = jwt.decode(token, { 20 | json: true, 21 | }); 22 | 23 | const user = await getUserById(decodedToken?.['id']); 24 | 25 | return user ? await getUserPublicFromUser(user) : undefined; 26 | } catch (err) { 27 | logger.error(err); 28 | 29 | return undefined; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "noEmit": true, 8 | "rootDir": ".", 9 | "target": "ESNext", 10 | "tsBuildInfoFile": "../../.cache/tsc/demo.tsbuildinfo", 11 | "types": ["vite/client"], 12 | "useDefineForClassFields": true, 13 | "useUnknownInCatchVariables": false, 14 | "verbatimModuleSyntax": false 15 | }, 16 | "include": ["./src", "./vite.config.ts"], 17 | "exclude": ["**/*.test.ts"], 18 | "references": [ 19 | { "path": "../packages/core/tsconfig.esm.json" }, 20 | { "path": "../packages/react/tsconfig.esm.json" }, 21 | { "path": "../packages/web-sdk/tsconfig.esm.json" }, 22 | { "path": "../packages/web-tracing/tsconfig.esm.json" }, 23 | { "path": "../experimental/instrumentation-fetch/tsconfig.esm.json" }, 24 | { "path": "../experimental/instrumentation-xhr/tsconfig.esm.json" } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Faro Documentation 2 | 3 | This directory contains documentation for Faro. 4 | -------------------------------------------------------------------------------- /docs/faro_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/docs/faro_logo.png -------------------------------------------------------------------------------- /docs/sources/developer/architecture/components/instrumentations.md: -------------------------------------------------------------------------------- 1 | # Instrumentations 2 | 3 | Instrumentations are the data collectors in the Faro architecture. They are responsible for gathering the data from 4 | various APIs like the browesr APIs or by other means. 5 | 6 | The core library does not provide any instrumentations out of the box and they are either provided by wrapper packages 7 | like `web-sdk` or by the user. 8 | 9 | ## Instrumentations SDK 10 | 11 | The instrumentations SDK is the internal handler for the instrumentations component. It is responsible for keeping track 12 | of the initialized instrumentations as well as adding others and removing existing ones. 13 | 14 | Methods and properties: 15 | 16 | - `add()` - adds a new instrumentation 17 | - `remove()` - removes a specific instrumentation 18 | - `instrumentations` - accesses the current list of initialized instrumentations 19 | -------------------------------------------------------------------------------- /docs/sources/developer/architecture/components/unpatched-console.md: -------------------------------------------------------------------------------- 1 | # Unpatched Console 2 | 3 | The unpatched console refers to an unmodified `console` object that is used for various purposes like defining the 4 | internal Faro logger or pointing to an alternative to the `console` object for the apps that are using a custom logger 5 | instead. 6 | 7 | The unpatched console is the first thing that is initialized when initializing Faro. It is then made available to all 8 | components like internal logger, instrumentations etc. 9 | -------------------------------------------------------------------------------- /docs/sources/developer/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | To release a new version, run `npx lerna version --force-publish` on the main branch. 4 | It will ask some questions, bump package versions, tag & push. 5 | CI will pick it up from there and publish to npm. 6 | 7 | **Note:** 8 | Before calling `npx lerna version --force-publish` always ensure that your local main branch is 1:1 with origin/main. 9 | 10 | - Do `git pull` the main branch 11 | - Check `git status` to double check if you have any unpushed changes 12 | 13 | **Note 2:** 14 | You need special access privileges to be able push to the protected main branch. 15 | It's recommended to protect the main branch with your local tooling. 16 | 17 | For VsCode 18 | 19 | - Add your branch to the `git.branchProtection` property 20 | - Set `git.branchProtectionPrompt` to `always prompt` 21 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['experimental/instrumentation-fetch/src'], 6 | testEnvironment: 'jsdom', 7 | setupFiles: ['/experimental/instrumentation-fetch/src/setupTests.ts'], 8 | moduleNameMapper: { 9 | '^@remix-run/router$': '/index.ts', 10 | '^@remix-run/web-blob$': require.resolve('@remix-run/web-blob'), 11 | '^@remix-run/web-fetch$': require.resolve('@remix-run/web-fetch'), 12 | '^@remix-run/web-form-data$': require.resolve('@remix-run/web-form-data'), 13 | '^@remix-run/web-stream$': require.resolve('@remix-run/web-stream'), 14 | '^@web3-storage/multipart-parser$': require.resolve('@web3-storage/multipart-parser'), 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('instrumentationFetch'); 4 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | export { FetchInstrumentation } from './instrumentation'; 2 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { fetch, Request, Response } from '@remix-run/web-fetch'; 2 | 3 | if (!globalThis.fetch) { 4 | // @ts-expect-error 5 | globalThis.fetch = fetch; 6 | 7 | // @ts-expect-error 8 | globalThis.Request = Request; 9 | 10 | // @ts-expect-error 11 | globalThis.Response = Response; 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '@grafana/faro-core'; 2 | 3 | /** 4 | * Interface used to provide information to finish span on fetch error 5 | */ 6 | export interface FetchError { 7 | status?: number; 8 | message: string; 9 | } 10 | 11 | export interface FetchInstrumentationOptions { 12 | // For these URLs no events will be tracked 13 | ignoredUrls?: Patterns; 14 | // For testing purposes - if true, fetch will be writable - necessary for jest tests 15 | testing?: boolean; 16 | 17 | /** 18 | * RUM headers are only added to URLs which have the same origin as the document. 19 | * Ad other URLs which should have RUM headers added to this list. 20 | */ 21 | propagateRumHeaderCorsUrls?: Patterns; 22 | } 23 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationFetch.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationFetch.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noPropertyAccessFromIndexSignature": false 4 | }, 5 | "references": [ 6 | { "path": "./tsconfig.cjs.json" }, 7 | { "path": "./tsconfig.esm.json" }, 8 | { "path": "./tsconfig.spec.json" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /experimental/instrumentation-fetch/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "../../", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationFetch.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['experimental/instrumentation-performance-timeline/src'], 6 | testEnvironment: 'jsdom', 7 | }; 8 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('instrumentationPerformanceTimeline'); 4 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/src/index.ts: -------------------------------------------------------------------------------- 1 | export { PerformanceTimelineInstrumentation, DEFAULT_PERFORMANCE_TIMELINE_ENTRY_TYPES } from './instrumentation'; 2 | 3 | export type { PerformanceTimelineInstrumentationOptions } from './types'; 4 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '@grafana/faro-core'; 2 | 3 | export type ObserveEntries = { type: string; buffered: boolean; [key: string]: any }; 4 | 5 | export interface PerformanceTimelineInstrumentationOptions { 6 | // The Performance Entry types which shall be observed 7 | observeEntryTypes?: ObserveEntries[]; 8 | 9 | // The size of the browser's resource timing buffer which stores the "resource" performance entries. 10 | resourceTimingBufferSize?: number; 11 | 12 | // If resource buffer size is full, set this as the new 13 | maxResourceTimingBufferSize?: number; 14 | 15 | // For these URLs no events will be tracked 16 | ignoredUrls?: Patterns; 17 | 18 | // Mutate performance entry before emit. Return false if entry shall be skipped. Parameter is the JSON representation of the PerformanceEntry as returned by calling it's own toJson() function. 19 | beforeEmit?: (performanceEntryJSON: any) => Record | false; 20 | } 21 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationPerformanceTimeline.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationPerformanceTimeline.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /experimental/instrumentation-performance-timeline/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "../../", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationPerformanceTimeline.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['experimental/instrumentation-xhr/src'], 6 | testEnvironment: 'jsdom', 7 | moduleNameMapper: { 8 | '^@remix-run/router$': '/index.ts', 9 | '^@remix-run/web-blob$': require.resolve('@remix-run/web-blob'), 10 | '^@remix-run/web-fetch$': require.resolve('@remix-run/web-fetch'), 11 | '^@remix-run/web-form-data$': require.resolve('@remix-run/web-form-data'), 12 | '^@remix-run/web-stream$': require.resolve('@remix-run/web-stream'), 13 | '^@web3-storage/multipart-parser$': require.resolve('@web3-storage/multipart-parser'), 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('instrumentationXHR'); 4 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/src/index.ts: -------------------------------------------------------------------------------- 1 | export { XHRInstrumentation } from './instrumentation'; 2 | 3 | export { parseXHREvent } from './utils'; 4 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '@grafana/faro-core'; 2 | 3 | export interface XHRInstrumentationOptions { 4 | // For these URLs no events will be tracked 5 | ignoredUrls?: Patterns; 6 | propagateRumHeaderCorsUrls?: Patterns; 7 | } 8 | 9 | export enum XHREventType { 10 | LOAD = 'faro.xhr.load', 11 | ABORT = 'faro.xhr.abort', 12 | ERROR = 'faro.xhr.error', 13 | TIMEOUT = 'faro.xhr.timeout', 14 | } 15 | 16 | export const serverTimingHeader = 'server-timing'; 17 | export const faroRumHeader = 'x-faro-session'; 18 | 19 | export const makeFaroRumHeaderValue = (sessionId: string): string => { 20 | return `session_id=${sessionId}`; 21 | }; 22 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationXHR.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationXHR.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /experimental/instrumentation-xhr/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "../../", 7 | "tsBuildInfoFile": "../../.cache/tsc/instrumentationXHR.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /experimental/transport-otlp-http/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['experimental/transport-otlp-http/src'], 6 | testEnvironment: 'jsdom', 7 | }; 8 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('transportOtlpHttp'); 4 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/src/index.ts: -------------------------------------------------------------------------------- 1 | export { OtlpHttpTransport } from './transport'; 2 | export type { OtlpHttpTransportOptions, OtlpTransportRequestOptions } from './types'; 3 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/src/payload/attribute/index.ts: -------------------------------------------------------------------------------- 1 | export { toAttribute, toAttributeValue, isAttribute } from './attributeUtils'; 2 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/src/payload/index.ts: -------------------------------------------------------------------------------- 1 | export { OtelPayload } from './OtelPayload'; 2 | export type { 3 | LogRecord, 4 | LogTransportItem, 5 | Resource, 6 | ResourceLog, 7 | ResourceLogs, 8 | Scope, 9 | ScopeLog, 10 | } from './transform/index'; 11 | export type { Logs, OtelTransportPayload, Traces } from './types'; 12 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/src/payload/transform/index.ts: -------------------------------------------------------------------------------- 1 | export { getLogTransforms, getTraceTransforms } from './transform'; 2 | export type { 3 | LogRecord, 4 | LogTransportItem, 5 | ResourceLogs, 6 | LogsTransform, 7 | Resource, 8 | ResourceLog, 9 | ResourceSpan, 10 | ResourceSpans, 11 | Scope, 12 | ScopeLog, 13 | TraceTransform, 14 | StringValueNonNullable, 15 | } from './types'; 16 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/src/payload/types.ts: -------------------------------------------------------------------------------- 1 | import type { ResourceLogs, ResourceSpans } from './transform'; 2 | 3 | export interface OtelTransportPayload { 4 | readonly resourceLogs: Readonly; 5 | readonly resourceSpans: Readonly; 6 | } 7 | 8 | export type Logs = Pick; 9 | export type Traces = Pick; 10 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/transportOtlpHttp.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/transportOtlpHttp.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /experimental/transport-otlp-http/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "../../", 7 | "tsBuildInfoFile": "../../.cache/tsc/transportOtlpHttp.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../../packages/core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /infra/grafana/config/grafana.ini: -------------------------------------------------------------------------------- 1 | [analytics] 2 | reporting_enabled = false 3 | 4 | [auth.anonymous] 5 | enabled = true 6 | org_role = Admin 7 | 8 | [explore] 9 | enabled = true 10 | 11 | [users] 12 | default_theme = dark 13 | 14 | [feature_toggles] 15 | enable = tempoServiceGraph tempoSearch tempoBackendSearch traceToMetrics traceqlEditor topNav 16 | 17 | [plugins] 18 | allow_loading_unsigned_plugins = grafana-kowalski-app 19 | -------------------------------------------------------------------------------- /infra/grafana/dashboards-provisioning/dashboards.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'grafanaAlloy' 5 | orgId: 1 6 | folder: 'Grafana Alloy' 7 | folderUid: '' 8 | type: 'file' 9 | disableDeletion: true 10 | editable: true 11 | updateIntervalSeconds: 10 12 | allowUiUpdates: false 13 | options: 14 | path: '${GRAFANA_DASHBOARDS_PATH}' 15 | -------------------------------------------------------------------------------- /infra/grafana/plugins-provisioning/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/infra/grafana/plugins-provisioning/.gitkeep -------------------------------------------------------------------------------- /infra/postgres/config/config.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/faro-web-sdk/8bed77be5a3e05938d3698150f94285f60f06baf/infra/postgres/config/config.ini -------------------------------------------------------------------------------- /jest.config.base.js: -------------------------------------------------------------------------------- 1 | exports.jestBaseConfig = { 2 | verbose: true, 3 | moduleNameMapper: { 4 | '@grafana/faro-core/src/(.*)': '/packages/core/src/$1', 5 | }, 6 | rootDir: '../../', 7 | testEnvironment: 'node', 8 | transform: { 9 | '^.+\\.ts?$': [ 10 | 'ts-jest', 11 | { 12 | tsconfig: 'tsconfig.spec.json', 13 | }, 14 | ], 15 | }, 16 | testEnvironment: 'jsdom', 17 | }; 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "experimental/*", 5 | "demo" 6 | ], 7 | "version": "1.18.2", 8 | "npmClient": "yarn", 9 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 10 | } -------------------------------------------------------------------------------- /packages/core/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /packages/core/bin/genVersion.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('node:fs'); 2 | const { join } = require('node:path'); 3 | 4 | if (process.env['SKIP_GEN_VERSION'] && process.env['SKIP_GEN_VERSION'] === '1') { 5 | console.info('Skipping generating version file due to "SKIP_GEN_VERSION" environment variable'); 6 | process.exit(0); 7 | } 8 | 9 | let version = ''; 10 | 11 | try { 12 | const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8')); 13 | version = packageJson.version; 14 | 15 | if (!version) { 16 | throw new Error('No version found in package.json'); 17 | } 18 | } catch (err) { 19 | console.error('Could not generate version file'); 20 | console.error(err); 21 | process.exit(1); 22 | } 23 | 24 | writeFileSync( 25 | 'src/version.ts', 26 | `// auto-generated by bin/genVersion.ts 27 | export const VERSION = '${version}'; 28 | ` 29 | ); 30 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['packages/core/src'], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('core'); 4 | -------------------------------------------------------------------------------- /packages/core/src/api/ItemBuffer.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../utils/is'; 2 | 3 | export class ItemBuffer { 4 | private buffer: T[]; 5 | 6 | constructor() { 7 | this.buffer = []; 8 | } 9 | 10 | addItem(item: T) { 11 | this.buffer.push(item); 12 | } 13 | 14 | flushBuffer(cb?: (item: T) => void) { 15 | if (isFunction(cb)) { 16 | for (const item of this.buffer) { 17 | cb(item); 18 | } 19 | } 20 | 21 | this.buffer.length = 0; 22 | } 23 | 24 | size() { 25 | return this.buffer.length; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/api/apiTestHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { Transports } from '../transports'; 2 | 3 | import type { TracesAPI } from './traces/types'; 4 | 5 | export const mockMetas = { 6 | add: jest.fn(), 7 | remove: jest.fn(), 8 | addListener: jest.fn(), 9 | removeListener: jest.fn(), 10 | value: {}, 11 | }; 12 | 13 | export const mockTransports: Transports = { 14 | add: jest.fn(), 15 | addBeforeSendHooks: jest.fn(), 16 | execute: jest.fn(), 17 | getBeforeSendHooks: jest.fn(), 18 | remove: jest.fn(), 19 | removeBeforeSendHooks: jest.fn(), 20 | isPaused: function (): boolean { 21 | throw new Error('Function not implemented.'); 22 | }, 23 | transports: [], 24 | pause: function (): void { 25 | throw new Error('Function not implemented.'); 26 | }, 27 | unpause: function (): void { 28 | throw new Error('Function not implemented.'); 29 | }, 30 | }; 31 | 32 | export const mockTracesApi: TracesAPI = { 33 | getOTEL: jest.fn(), 34 | getTraceContext: jest.fn(), 35 | initOTEL: jest.fn(), 36 | isOTELInitialized: jest.fn(), 37 | pushTraces: jest.fn(), 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/src/api/const.ts: -------------------------------------------------------------------------------- 1 | export const USER_ACTION_START = 'user-action-start'; 2 | export const USER_ACTION_END = 'user-action-end'; 3 | export const USER_ACTION_CANCEL = 'user-action-cancel'; 4 | export const USER_ACTION_HALT = 'user-action-halt'; 5 | -------------------------------------------------------------------------------- /packages/core/src/api/events/index.ts: -------------------------------------------------------------------------------- 1 | export type { EventAttributes, EventEvent, EventsAPI, PushEventOptions } from './types'; 2 | export { initializeEventsAPI } from './initialize'; 3 | -------------------------------------------------------------------------------- /packages/core/src/api/exceptions/const.ts: -------------------------------------------------------------------------------- 1 | import { isObject, stringifyExternalJson } from '../../utils'; 2 | 3 | export const defaultExceptionType = 'Error'; 4 | 5 | export const defaultErrorArgsSerializer = (args: [any?, ...any[]]) => { 6 | return args 7 | .map((arg) => { 8 | if (isObject(arg)) { 9 | return stringifyExternalJson(arg); 10 | } 11 | 12 | return String(arg); 13 | }) 14 | .join(' '); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/core/src/api/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultExceptionType, defaultErrorArgsSerializer } from './const'; 2 | 3 | export { initializeExceptionsAPI } from './initialize'; 4 | 5 | export type { 6 | ExceptionEvent, 7 | ExceptionStackFrame, 8 | ExceptionsAPI, 9 | ExtendedError, 10 | PushErrorOptions, 11 | Stacktrace, 12 | StacktraceParser, 13 | ErrorWithIndexProperties, 14 | ExceptionEventExtended, 15 | } from './types'; 16 | -------------------------------------------------------------------------------- /packages/core/src/api/logs/const.ts: -------------------------------------------------------------------------------- 1 | import type { LogArgsSerializer } from './types'; 2 | 3 | export const defaultLogArgsSerializer: LogArgsSerializer = (args) => 4 | args 5 | .map((arg) => { 6 | try { 7 | return String(arg); 8 | } catch (err) { 9 | return ''; 10 | } 11 | }) 12 | .join(' '); 13 | -------------------------------------------------------------------------------- /packages/core/src/api/logs/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultLogArgsSerializer } from './const'; 2 | 3 | export { initializeLogsAPI } from './initialize'; 4 | 5 | export type { LogContext, LogEvent, LogArgsSerializer, LogsAPI, PushLogOptions } from './types'; 6 | -------------------------------------------------------------------------------- /packages/core/src/api/logs/types.ts: -------------------------------------------------------------------------------- 1 | import type { SpanContext } from '@opentelemetry/api'; 2 | 3 | import type { LogLevel } from '../../utils'; 4 | import type { TraceContext } from '../traces'; 5 | import type { UserAction } from '../types'; 6 | 7 | export type LogContext = Record; 8 | 9 | export interface LogEvent { 10 | context: LogContext | undefined; 11 | level: LogLevel; 12 | message: string; 13 | timestamp: string; 14 | 15 | trace?: TraceContext; 16 | action?: UserAction; 17 | } 18 | 19 | export interface PushLogOptions { 20 | context?: LogContext; 21 | level?: LogLevel; 22 | skipDedupe?: boolean; 23 | spanContext?: Pick; 24 | timestampOverwriteMs?: number; 25 | } 26 | 27 | export interface LogsAPI { 28 | pushLog: (args: unknown[], options?: PushLogOptions) => void; 29 | } 30 | 31 | export type LogArgsSerializer = (args: unknown[]) => string; 32 | -------------------------------------------------------------------------------- /packages/core/src/api/measurements/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeMeasurementsAPI } from './initialize'; 2 | 3 | export type { MeasurementEvent, MeasurementsAPI, PushMeasurementOptions } from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/src/api/measurements/types.ts: -------------------------------------------------------------------------------- 1 | import type { SpanContext } from '@opentelemetry/api'; 2 | 3 | import type { TraceContext } from '../traces'; 4 | import type { UserAction } from '../types'; 5 | 6 | export type MeasurementContext = Record; 7 | 8 | export interface MeasurementEvent { 9 | type: string; 10 | values: V; 11 | 12 | timestamp: string; 13 | trace?: TraceContext; 14 | context?: MeasurementContext; 15 | 16 | action?: UserAction; 17 | } 18 | 19 | export interface PushMeasurementOptions { 20 | skipDedupe?: boolean; 21 | context?: MeasurementContext; 22 | spanContext?: Pick; 23 | timestampOverwriteMs?: number; 24 | } 25 | 26 | export interface MeasurementsAPI { 27 | pushMeasurement: ( 28 | payload: Omit & Partial>, 29 | options?: PushMeasurementOptions 30 | ) => void; 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/api/meta/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeMetaAPI } from './initialize'; 2 | 3 | export type { MetaAPI } from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/src/api/meta/types.ts: -------------------------------------------------------------------------------- 1 | import type { MetaOverrides, MetaPage, MetaSession, MetaUser, MetaView } from '../../metas'; 2 | 3 | type OverridesAvailableThroughApi = Pick; 4 | 5 | export interface MetaAPI { 6 | setUser: (user?: MetaUser | undefined) => void; 7 | resetUser: () => void; 8 | setSession: ( 9 | session?: MetaSession | undefined, 10 | options?: { 11 | overrides: OverridesAvailableThroughApi; 12 | } 13 | ) => void; 14 | resetSession: () => void; 15 | getSession: () => MetaSession | undefined; 16 | setView: ( 17 | view?: MetaView | undefined, 18 | options?: { 19 | overrides: OverridesAvailableThroughApi; 20 | } 21 | ) => void; 22 | getView: () => MetaView | undefined; 23 | /** 24 | * If a string is provided, it will be used as the page id. 25 | */ 26 | setPage: (page?: MetaPage | string | undefined) => void; 27 | getPage: () => MetaPage | undefined; 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/api/traces/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeTracesAPI } from './initialize'; 2 | 3 | export type { OTELApi, TraceContext, TraceEvent, TracesAPI } from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/src/api/utils.test.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '..'; 2 | 3 | import { shouldIgnoreEvent } from './utils'; 4 | 5 | describe('api/utils', () => { 6 | it('should ignore event', () => { 7 | let patterns: Patterns = ['pattern1', 'pattern2']; 8 | let msg = 'message pattern1'; 9 | let result = shouldIgnoreEvent(patterns, msg); 10 | expect(result).toBe(true); 11 | 12 | patterns = ['pattern1', /foo/]; 13 | msg = 'This is a foo example'; 14 | result = shouldIgnoreEvent(patterns, msg); 15 | expect(result).toBe(true); 16 | 17 | patterns = ['pattern1', /foo/]; 18 | msg = "This example doesn't match"; 19 | result = shouldIgnoreEvent(patterns, msg); 20 | expect(result).toBe(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/src/api/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '../config/types'; 2 | import { isString } from '../utils/is'; 3 | 4 | export function shouldIgnoreEvent(patterns: Patterns, msg: string): boolean { 5 | return patterns.some((pattern) => { 6 | return isString(pattern) ? msg.includes(pattern) : !!msg.match(pattern); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/config/const.ts: -------------------------------------------------------------------------------- 1 | export const defaultGlobalObjectKey = 'faro'; 2 | 3 | export const defaultBatchingConfig = { 4 | enabled: true, 5 | sendTimeout: 250, 6 | itemLimit: 50, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultBatchingConfig, defaultGlobalObjectKey } from './const'; 2 | 3 | export type { Config, Patterns } from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const unknownString = 'unknown'; 2 | -------------------------------------------------------------------------------- /packages/core/src/extensions/baseExtension.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../config'; 2 | import { defaultInternalLogger } from '../internalLogger'; 3 | import type { Metas } from '../metas'; 4 | import { defaultUnpatchedConsole } from '../unpatchedConsole'; 5 | 6 | import type { Extension } from './types'; 7 | 8 | export abstract class BaseExtension implements Extension { 9 | abstract readonly name: string; 10 | abstract readonly version: string; 11 | 12 | unpatchedConsole = defaultUnpatchedConsole; 13 | internalLogger = defaultInternalLogger; 14 | config = {} as Config; 15 | metas = {} as Metas; 16 | 17 | logDebug(...args: unknown[]): void { 18 | this.internalLogger.debug(`${this.name}\n`, ...args); 19 | } 20 | 21 | logInfo(...args: unknown[]): void { 22 | this.internalLogger.info(`${this.name}\n`, ...args); 23 | } 24 | 25 | logWarn(...args: unknown[]): void { 26 | this.internalLogger.warn(`${this.name}\n`, ...args); 27 | } 28 | 29 | logError(...args: unknown[]): void { 30 | this.internalLogger.error(`${this.name}\n`, ...args); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseExtension } from './baseExtension'; 2 | 3 | export type { Extension } from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/src/extensions/types.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../config'; 2 | import type { InternalLogger } from '../internalLogger'; 3 | import type { Metas } from '../metas'; 4 | import type { UnpatchedConsole } from '../unpatchedConsole'; 5 | 6 | export interface Extension { 7 | readonly name: string; 8 | readonly version: string; 9 | 10 | internalLogger: InternalLogger; 11 | unpatchedConsole: UnpatchedConsole; 12 | config: Config; 13 | metas: Metas; 14 | 15 | logDebug(...args: unknown[]): void; 16 | logInfo(...args: unknown[]): void; 17 | logWarn(...args: unknown[]): void; 18 | logError(...args: unknown[]): void; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/globalObject/globalObject.ts: -------------------------------------------------------------------------------- 1 | import type { Faro, internalGlobalObjectKey } from '../sdk'; 2 | 3 | export type GlobalObject = T & { 4 | [label: string]: Faro; 5 | 6 | [internalGlobalObjectKey]: Faro; 7 | }; 8 | 9 | // This does not uses isUndefined method because it will throw an error in non-browser environments 10 | export const globalObject = (typeof globalThis !== 'undefined' 11 | ? globalThis 12 | : typeof global !== 'undefined' 13 | ? global 14 | : typeof self !== 'undefined' 15 | ? self 16 | : undefined) as unknown as GlobalObject; 17 | -------------------------------------------------------------------------------- /packages/core/src/globalObject/index.ts: -------------------------------------------------------------------------------- 1 | export { globalObject } from './globalObject'; 2 | export type { GlobalObject } from './globalObject'; 3 | -------------------------------------------------------------------------------- /packages/core/src/instrumentations/base.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '../api'; 2 | import { BaseExtension } from '../extensions'; 3 | import type { Transports } from '../transports'; 4 | 5 | import type { Instrumentation } from './types'; 6 | 7 | export abstract class BaseInstrumentation extends BaseExtension implements Instrumentation { 8 | api: API = {} as API; 9 | transports: Transports = {} as Transports; 10 | 11 | abstract initialize(): void; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/instrumentations/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseInstrumentation } from './base'; 2 | 3 | export { initializeInstrumentations } from './initialize'; 4 | 5 | export { registerInitialInstrumentations } from './registerInitial'; 6 | 7 | export type { Instrumentation, Instrumentations } from './types'; 8 | -------------------------------------------------------------------------------- /packages/core/src/instrumentations/registerInitial.ts: -------------------------------------------------------------------------------- 1 | import type { Faro } from '../sdk'; 2 | 3 | export function registerInitialInstrumentations(faro: Faro): void { 4 | faro.instrumentations.add(...faro.config.instrumentations); 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/instrumentations/types.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '../api'; 2 | import type { Extension } from '../extensions'; 3 | import type { Transports } from '../transports'; 4 | 5 | export interface Instrumentation extends Extension { 6 | api: API; 7 | transports: Transports; 8 | 9 | initialize: VoidFunction; 10 | 11 | destroy?: VoidFunction; 12 | } 13 | 14 | export interface Instrumentations { 15 | add: (...instrumentations: Instrumentation[]) => void; 16 | instrumentations: Instrumentation[]; 17 | remove: (...instrumentations: Instrumentation[]) => void; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/internalLogger/const.ts: -------------------------------------------------------------------------------- 1 | import { noop } from '../utils'; 2 | 3 | import type { InternalLogger } from './types'; 4 | 5 | export enum InternalLoggerLevel { 6 | OFF = 0, 7 | ERROR = 1, 8 | WARN = 2, 9 | INFO = 3, 10 | VERBOSE = 4, 11 | } 12 | 13 | export const defaultInternalLoggerPrefix = 'Faro'; 14 | 15 | export const defaultInternalLogger: InternalLogger = { 16 | debug: noop, 17 | error: noop, 18 | info: noop, 19 | prefix: defaultInternalLoggerPrefix, 20 | warn: noop, 21 | } as const; 22 | 23 | export const defaultInternalLoggerLevel = InternalLoggerLevel.ERROR; 24 | -------------------------------------------------------------------------------- /packages/core/src/internalLogger/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultInternalLogger, defaultInternalLoggerLevel, InternalLoggerLevel } from './const'; 2 | 3 | export { createInternalLogger } from './createInternalLogger'; 4 | 5 | export { initializeInternalLogger, internalLogger } from './initialize'; 6 | 7 | export type { InternalLogger } from './types'; 8 | -------------------------------------------------------------------------------- /packages/core/src/internalLogger/initialize.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../config'; 2 | import type { UnpatchedConsole } from '../unpatchedConsole'; 3 | 4 | import { defaultInternalLogger } from './const'; 5 | import { createInternalLogger } from './createInternalLogger'; 6 | import type { InternalLogger } from './types'; 7 | 8 | export let internalLogger: InternalLogger = defaultInternalLogger; 9 | 10 | export function initializeInternalLogger(unpatchedConsole: UnpatchedConsole, config: Config): InternalLogger { 11 | internalLogger = createInternalLogger(unpatchedConsole, config.internalLoggerLevel); 12 | 13 | return internalLogger; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/internalLogger/types.ts: -------------------------------------------------------------------------------- 1 | export interface InternalLogger { 2 | debug: Console['debug']; 3 | error: Console['error']; 4 | info: Console['info']; 5 | readonly prefix: string; 6 | warn: Console['warn']; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/metas/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeMetas } from './initialize'; 2 | 3 | export { registerInitialMetas } from './registerInitial'; 4 | 5 | export type { 6 | Meta, 7 | MetaApp, 8 | MetaAttributes, 9 | MetaBrowser, 10 | MetaGetter, 11 | MetaItem, 12 | MetaK6, 13 | MetaPage, 14 | MetaSDK, 15 | MetaSDKIntegration, 16 | MetaSession, 17 | MetaUser, 18 | MetaView, 19 | Metas, 20 | MetaOverrides, 21 | } from './types'; 22 | -------------------------------------------------------------------------------- /packages/core/src/metas/initialize.test.ts: -------------------------------------------------------------------------------- 1 | import { initializeFaro } from '../initialize'; 2 | import { mockConfig } from '../testUtils'; 3 | 4 | describe('metas', () => { 5 | it('can set listeners and they will be notified on meta changes', () => { 6 | const { metas } = initializeFaro(mockConfig()); 7 | 8 | const listener = jest.fn(() => {}); 9 | metas.addListener(listener); 10 | 11 | metas.add({ user: { id: 'foo' } }); 12 | expect(listener).toHaveBeenCalledTimes(1); 13 | expect(listener).toHaveBeenLastCalledWith(metas.value); 14 | 15 | metas.add({ session: { id: '1' } }); 16 | expect(listener).toHaveBeenCalledTimes(2); 17 | metas.removeListener(listener); 18 | 19 | metas.add({ session: { id: '2' } }); 20 | expect(listener).toHaveBeenCalledTimes(2); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/src/metas/registerInitial.ts: -------------------------------------------------------------------------------- 1 | import type { Faro } from '../sdk'; 2 | import { getBundleId } from '../utils/sourceMaps'; 3 | import { VERSION } from '../version'; 4 | 5 | import type { Meta } from './types'; 6 | 7 | export function registerInitialMetas(faro: Faro): void { 8 | const initial: Meta = { 9 | sdk: { 10 | version: VERSION, 11 | }, 12 | app: { 13 | bundleId: faro.config.app.name && getBundleId(faro.config.app.name), 14 | }, 15 | }; 16 | 17 | const session = faro.config.sessionTracking?.session; 18 | if (session) { 19 | faro.api.setSession(session); 20 | } 21 | 22 | if (faro.config.app) { 23 | initial.app = { ...faro.config.app, ...initial.app }; 24 | } 25 | 26 | if (faro.config.user) { 27 | initial.user = faro.config.user; 28 | } 29 | 30 | if (faro.config.view) { 31 | initial.view = faro.config.view; 32 | } 33 | 34 | faro.metas.add(initial, ...(faro.config.metas ?? [])); 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/sdk/const.ts: -------------------------------------------------------------------------------- 1 | export const internalGlobalObjectKey = '_faroInternal'; 2 | -------------------------------------------------------------------------------- /packages/core/src/sdk/faroGlobalObject.ts: -------------------------------------------------------------------------------- 1 | import { globalObject } from '../globalObject'; 2 | 3 | import type { Faro } from './types'; 4 | 5 | export function setFaroOnGlobalObject(faro: Faro): void { 6 | if (!faro.config.preventGlobalExposure) { 7 | faro.internalLogger.debug( 8 | `Registering public faro reference in the global scope using "${faro.config.globalObjectKey}" key` 9 | ); 10 | 11 | if (faro.config.globalObjectKey in globalObject) { 12 | faro.internalLogger.warn( 13 | `Skipping global registration due to key "${faro.config.globalObjectKey}" being used already. Please set "globalObjectKey" to something else or set "preventGlobalExposure" to "true"` 14 | ); 15 | 16 | return; 17 | } 18 | 19 | Object.defineProperty(globalObject, faro.config.globalObjectKey, { 20 | configurable: false, 21 | writable: false, 22 | value: faro, 23 | }); 24 | } else { 25 | faro.internalLogger.debug('Skipping registering public Faro instance in the global scope'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export { internalGlobalObjectKey } from './const'; 2 | 3 | export { faro, registerFaro } from './registerFaro'; 4 | 5 | export { 6 | getInternalFromGlobalObject as getInternalFaroFromGlobalObject, 7 | isInternalFaroOnGlobalObject as isInternalFaroOnGlobalObject, 8 | setInternalFaroOnGlobalObject as setInternalFaroOnGlobalObject, 9 | } from './internalFaroGlobalObject'; 10 | 11 | export type { Faro } from './types'; 12 | -------------------------------------------------------------------------------- /packages/core/src/sdk/internalFaroGlobalObject.ts: -------------------------------------------------------------------------------- 1 | import { globalObject } from '../globalObject'; 2 | 3 | import { internalGlobalObjectKey } from './const'; 4 | import type { Faro } from './types'; 5 | 6 | export function getInternalFromGlobalObject(): Faro | undefined { 7 | return globalObject[internalGlobalObjectKey]; 8 | } 9 | 10 | export function setInternalFaroOnGlobalObject(faro: Faro): void { 11 | if (!faro.config.isolate) { 12 | faro.internalLogger.debug('Registering internal Faro instance on global object'); 13 | 14 | Object.defineProperty(globalObject, internalGlobalObjectKey, { 15 | configurable: false, 16 | enumerable: false, 17 | writable: false, 18 | value: faro, 19 | }); 20 | } else { 21 | faro.internalLogger.debug('Skipping registering internal Faro instance on global object'); 22 | } 23 | } 24 | 25 | export function isInternalFaroOnGlobalObject(): boolean { 26 | return internalGlobalObjectKey in globalObject; 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/sdk/types.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '../api'; 2 | import type { Config } from '../config'; 3 | import type { Instrumentations } from '../instrumentations'; 4 | import type { InternalLogger } from '../internalLogger'; 5 | import type { Metas } from '../metas'; 6 | import type { Transports } from '../transports'; 7 | import type { UnpatchedConsole } from '../unpatchedConsole'; 8 | 9 | export interface Faro { 10 | api: API; 11 | config: Config; 12 | instrumentations: Instrumentations; 13 | internalLogger: InternalLogger; 14 | metas: Metas; 15 | pause: Transports['pause']; 16 | transports: Transports; 17 | unpatchedConsole: UnpatchedConsole; 18 | unpause: Transports['unpause']; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/semantic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @deprecated The conventions object will be removed in a future version 3 | */ 4 | export const Conventions = { 5 | /** 6 | * @deprecated The event names object will be removed in a future version 7 | */ 8 | EventNames: { 9 | CLICK: 'click', 10 | NAVIGATION: 'navigation', 11 | SESSION_START: 'session_start', 12 | VIEW_CHANGED: 'view_changed', 13 | }, 14 | } as const; 15 | 16 | export const EVENT_CLICK = 'click'; 17 | export const EVENT_NAVIGATION = 'navigation'; 18 | export const EVENT_VIEW_CHANGED = 'view_changed'; 19 | export const EVENT_SESSION_START = 'session_start'; 20 | export const EVENT_SESSION_RESUME = 'session_resume'; 21 | export const EVENT_SESSION_EXTEND = 'session_extend'; 22 | export const EVENT_OVERRIDES_SERVICE_NAME = 'service_name_override'; 23 | export const EVENT_ROUTE_CHANGE = 'route_change'; 24 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/index.ts: -------------------------------------------------------------------------------- 1 | export { mockConfig } from './mockConfig'; 2 | 3 | export { mockInternalLogger } from './mockInternalLogger'; 4 | 5 | export { mockStacktraceParser } from './mockStacktraceParser'; 6 | 7 | export { MockTransport } from './mockTransport'; 8 | 9 | export { createTestPromise } from './testPromise'; 10 | export type { TestPromise } from './testPromise'; 11 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/mockConfig.ts: -------------------------------------------------------------------------------- 1 | import { type Config, defaultBatchingConfig } from '../config'; 2 | import { defaultInternalLoggerLevel } from '../internalLogger'; 3 | 4 | import { mockStacktraceParser } from './mockStacktraceParser'; 5 | 6 | export function mockConfig(overrides: Partial = {}): Config { 7 | return { 8 | app: { 9 | name: 'test', 10 | version: '1.0.0', 11 | }, 12 | batching: { 13 | enabled: false, 14 | }, 15 | dedupe: true, 16 | globalObjectKey: 'faro', 17 | internalLoggerLevel: defaultInternalLoggerLevel, 18 | instrumentations: [], 19 | isolate: true, 20 | metas: [], 21 | parseStacktrace: mockStacktraceParser, 22 | paused: false, 23 | preventGlobalExposure: true, 24 | transports: [], 25 | unpatchedConsole: console, 26 | sessionTracking: { 27 | ...defaultBatchingConfig, 28 | }, 29 | ...overrides, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/mockInternalLogger.ts: -------------------------------------------------------------------------------- 1 | import type { InternalLogger } from '../internalLogger'; 2 | import { noop } from '../utils'; 3 | 4 | export const mockInternalLogger: InternalLogger = { 5 | prefix: 'Faro', 6 | debug: noop, 7 | info: noop, 8 | warn: noop, 9 | error: noop, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/mockStacktraceParser.ts: -------------------------------------------------------------------------------- 1 | import type { ExceptionStackFrame, StacktraceParser } from '../api'; 2 | 3 | export const mockStacktraceParser: StacktraceParser = (err) => { 4 | const frames: ExceptionStackFrame[] = []; 5 | const stack = err.stack ?? err.stacktrace; 6 | 7 | if (stack) { 8 | stack.split('\n').forEach((line) => { 9 | frames.push({ 10 | filename: line, 11 | function: '', 12 | }); 13 | }); 14 | } 15 | 16 | return { 17 | frames, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/mockTransport.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '../config'; 2 | import { BaseTransport } from '../transports'; 3 | import type { Transport, TransportItem } from '../transports'; 4 | import { VERSION } from '../version'; 5 | 6 | export class MockTransport extends BaseTransport implements Transport { 7 | readonly name = '@grafana/transport-mock'; 8 | readonly version = VERSION; 9 | 10 | items: TransportItem[] = []; 11 | 12 | constructor(private ignoreURLs: Patterns = []) { 13 | super(); 14 | } 15 | 16 | send(items: TransportItem[]): void | Promise { 17 | this.items.push(...items); 18 | } 19 | 20 | override isBatched(): boolean { 21 | return true; 22 | } 23 | 24 | override getIgnoreUrls(): Patterns { 25 | return this.ignoreURLs; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/testUtils/testPromise.ts: -------------------------------------------------------------------------------- 1 | // exposes resolve, reject methods and 2 | // adds id 3 | export interface TestPromise { 4 | promise: Promise; 5 | reject: (reason: any) => void; 6 | resolve: (value: T | PromiseLike) => void; 7 | 8 | id?: number; 9 | } 10 | 11 | export function createTestPromise(id?: number) { 12 | const obj: TestPromise = { 13 | id, 14 | } as TestPromise; 15 | 16 | obj.promise = new Promise((resolve, reject) => { 17 | obj.resolve = resolve; 18 | obj.reject = reject; 19 | }); 20 | 21 | return obj; 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/transports/base.ts: -------------------------------------------------------------------------------- 1 | import type { Patterns } from '..'; 2 | import { BaseExtension } from '../extensions'; 3 | 4 | import type { Transport, TransportItem } from './types'; 5 | 6 | export abstract class BaseTransport extends BaseExtension implements Transport { 7 | abstract send(items: TransportItem | TransportItem[]): void | Promise; 8 | 9 | isBatched(): boolean { 10 | return false; 11 | } 12 | 13 | getIgnoreUrls(): Patterns { 14 | return []; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/transports/const.ts: -------------------------------------------------------------------------------- 1 | import type { BodyKey } from './types'; 2 | 3 | export enum TransportItemType { 4 | EXCEPTION = 'exception', 5 | LOG = 'log', 6 | MEASUREMENT = 'measurement', 7 | TRACE = 'trace', 8 | EVENT = 'event', 9 | } 10 | 11 | export const transportItemTypeToBodyKey: Record = { 12 | [TransportItemType.EXCEPTION]: 'exceptions', 13 | [TransportItemType.LOG]: 'logs', 14 | [TransportItemType.MEASUREMENT]: 'measurements', 15 | [TransportItemType.TRACE]: 'traces', 16 | [TransportItemType.EVENT]: 'events', 17 | } as const; 18 | -------------------------------------------------------------------------------- /packages/core/src/transports/index.ts: -------------------------------------------------------------------------------- 1 | export { initializeTransports } from './initialize'; 2 | 3 | export { BaseTransport } from './base'; 4 | 5 | export { TransportItemType, transportItemTypeToBodyKey } from './const'; 6 | 7 | export { registerInitialTransports } from './registerInitial'; 8 | 9 | export type { 10 | BatchExecutorOptions, 11 | BeforeSendHook, 12 | SendFn, 13 | Transport, 14 | TransportBody, 15 | TransportItem, 16 | TransportItemPayload, 17 | Transports, 18 | } from './types'; 19 | 20 | export { getTransportBody } from './utils'; 21 | -------------------------------------------------------------------------------- /packages/core/src/transports/registerInitial.ts: -------------------------------------------------------------------------------- 1 | import type { Faro } from '../sdk'; 2 | 3 | export function registerInitialTransports(faro: Faro): void { 4 | faro.transports.add(...faro.config.transports); 5 | faro.transports.addBeforeSendHooks(faro.config.beforeSend); 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/unpatchedConsole/const.ts: -------------------------------------------------------------------------------- 1 | import type { UnpatchedConsole } from './types'; 2 | 3 | export const defaultUnpatchedConsole: UnpatchedConsole = { ...console }; 4 | -------------------------------------------------------------------------------- /packages/core/src/unpatchedConsole/index.ts: -------------------------------------------------------------------------------- 1 | export { defaultUnpatchedConsole } from './const'; 2 | 3 | export { initializeUnpatchedConsole, unpatchedConsole } from './initialize'; 4 | 5 | export type { UnpatchedConsole } from './types'; 6 | -------------------------------------------------------------------------------- /packages/core/src/unpatchedConsole/initialize.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '../config'; 2 | 3 | import { defaultUnpatchedConsole } from './const'; 4 | import type { UnpatchedConsole } from './types'; 5 | 6 | export let unpatchedConsole: UnpatchedConsole = defaultUnpatchedConsole; 7 | 8 | export function initializeUnpatchedConsole(config: Config): UnpatchedConsole { 9 | unpatchedConsole = config.unpatchedConsole ?? unpatchedConsole; 10 | 11 | return unpatchedConsole; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/unpatchedConsole/types.ts: -------------------------------------------------------------------------------- 1 | export type UnpatchedConsole = Console; 2 | -------------------------------------------------------------------------------- /packages/core/src/utils/baseObject.ts: -------------------------------------------------------------------------------- 1 | export type BaseObjectKey = string | number; 2 | 3 | export type BaseObjectPrimitiveValue = string | number | boolean | null | undefined; 4 | 5 | export type BaseObjectValue = BaseObjectPrimitiveValue | BaseObject | BaseObjectPrimitiveValue[]; 6 | 7 | export interface BaseObject { 8 | [key: BaseObjectKey]: BaseObjectValue; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function dateNow(): number { 2 | return Date.now(); 3 | } 4 | 5 | export function getCurrentTimestamp(): string { 6 | return new Date().toISOString(); 7 | } 8 | 9 | export function timestampToIsoString(value: number): string { 10 | return new Date(value).toISOString(); 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/is.test.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from './is'; 2 | 3 | describe('Meta API', () => { 4 | beforeEach(() => { 5 | jest.resetAllMocks(); 6 | jest.restoreAllMocks(); 7 | }); 8 | 9 | it('isEmpty() determines the empty state of a given value', () => { 10 | expect(isEmpty(null)).toBe(true); 11 | expect(isEmpty(undefined)).toBe(true); 12 | expect(isEmpty('')).toBe(true); 13 | expect(isEmpty([])).toBe(true); 14 | expect(isEmpty({})).toBe(true); 15 | 16 | expect(isEmpty(0)).toBe(false); 17 | expect(isEmpty('0')).toBe(false); 18 | expect(isEmpty([0])).toBe(false); 19 | expect(isEmpty({ key: 'value' })).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './is'; 2 | 3 | export function getCircularDependencyReplacer() { 4 | const valueSeen = new WeakSet(); 5 | return function (_key: string | Symbol, value: unknown) { 6 | if (isObject(value) && value !== null) { 7 | if (valueSeen.has(value)) { 8 | return null; 9 | } 10 | valueSeen.add(value); 11 | } 12 | return value; 13 | }; 14 | } 15 | 16 | type JSONObject = { 17 | [key: string]: JSONValue; 18 | }; 19 | type JSONArray = JSONValue[] & {}; 20 | type JSONValue = string | number | boolean | null | JSONObject | JSONArray; 21 | 22 | export function stringifyExternalJson(json: any = {}) { 23 | return JSON.stringify(json ?? {}, getCircularDependencyReplacer()); 24 | } 25 | 26 | export function stringifyObjectValues(obj: Record = {}) { 27 | const o: Record = {}; 28 | 29 | for (const [key, value] of Object.entries(obj)) { 30 | o[key] = isObject(value) && value !== null ? stringifyExternalJson(value) : String(value); 31 | } 32 | 33 | return o; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/utils/logLevels.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | TRACE = 'trace', 3 | DEBUG = 'debug', 4 | INFO = 'info', 5 | LOG = 'log', 6 | WARN = 'warn', 7 | ERROR = 'error', 8 | } 9 | 10 | export const defaultLogLevel = LogLevel.LOG; 11 | 12 | export const allLogLevels: Readonly>> = [ 13 | LogLevel.TRACE, 14 | LogLevel.DEBUG, 15 | LogLevel.INFO, 16 | LogLevel.LOG, 17 | LogLevel.WARN, 18 | LogLevel.ERROR, 19 | ] as const; 20 | -------------------------------------------------------------------------------- /packages/core/src/utils/noop.ts: -------------------------------------------------------------------------------- 1 | export function noop(): void {} 2 | -------------------------------------------------------------------------------- /packages/core/src/utils/shortId.ts: -------------------------------------------------------------------------------- 1 | const alphabet = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789'; 2 | 3 | export function genShortID(length = 10): string { 4 | return Array.from(Array(length)) 5 | .map(() => alphabet[Math.floor(Math.random() * alphabet.length)]!) 6 | .join(''); 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/utils/sourceMaps.test.ts: -------------------------------------------------------------------------------- 1 | import { getBundleId } from './sourceMaps'; 2 | 3 | describe('sourceMapUpload utils', () => { 4 | beforeAll(() => { 5 | delete (global as any).__faroBundleId_foo; 6 | }); 7 | 8 | afterAll(() => { 9 | delete (global as any).__faroBundleId_foo; 10 | }); 11 | 12 | it('can get the bundle ID from the global object', () => { 13 | expect(getBundleId('foo')).toBeUndefined(); 14 | 15 | (global as any).__faroBundleId_foo = 'bar'; 16 | expect(getBundleId('foo')).toEqual('bar'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/src/utils/sourceMaps.ts: -------------------------------------------------------------------------------- 1 | import { globalObject } from '../globalObject'; 2 | 3 | export function getBundleId(appName: string): string | undefined { 4 | return (globalObject as any)?.[`__faroBundleId_${appName}`]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "..", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /packages/react/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('react'); 4 | -------------------------------------------------------------------------------- /packages/react/src/dependencies.ts: -------------------------------------------------------------------------------- 1 | import type { API, InternalLogger } from '@grafana/faro-web-sdk'; 2 | 3 | export let internalLogger: InternalLogger; 4 | export let api: API; 5 | 6 | export function setDependencies(newInternalLogger: InternalLogger, newApi: API): void { 7 | internalLogger = newInternalLogger; 8 | api = newApi; 9 | } 10 | -------------------------------------------------------------------------------- /packages/react/src/errorBoundary/const.ts: -------------------------------------------------------------------------------- 1 | import type { FaroErrorBoundaryState } from './types'; 2 | 3 | export const faroErrorBoundaryInitialState: FaroErrorBoundaryState = { 4 | hasError: false, 5 | error: null, 6 | } as const; 7 | -------------------------------------------------------------------------------- /packages/react/src/errorBoundary/index.ts: -------------------------------------------------------------------------------- 1 | export { faroErrorBoundaryInitialState } from './const'; 2 | 3 | export { FaroErrorBoundary } from './FaroErrorBoundary'; 4 | 5 | export type { 6 | FaroErrorBoundaryFallbackRender, 7 | FaroErrorBoundaryProps, 8 | FaroErrorBoundaryState, 9 | ReactNodeRender, 10 | ReactProps, 11 | } from './types'; 12 | 13 | export { withFaroErrorBoundary } from './withFaroErrorBoundary'; 14 | -------------------------------------------------------------------------------- /packages/react/src/errorBoundary/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactElement, ReactNode } from 'react'; 2 | 3 | import type { PushErrorOptions } from '@grafana/faro-web-sdk'; 4 | 5 | export type ReactNodeRender = () => ReactNode; 6 | 7 | export type ReactProps = Record; 8 | 9 | export type FaroErrorBoundaryFallbackRender = (error: Error, resetError: VoidFunction) => ReactElement; 10 | 11 | export interface FaroErrorBoundaryProps { 12 | beforeCapture?: (error: Error | null) => void; 13 | children?: ReactNode | ReactNodeRender; 14 | fallback?: ReactElement | FaroErrorBoundaryFallbackRender; 15 | onError?: (error: Error) => void; 16 | onMount?: VoidFunction; 17 | onReset?: (error: Error | null) => void; 18 | onUnmount?: (error: Error | null) => void; 19 | pushErrorOptions?: PushErrorOptions; 20 | } 21 | 22 | export interface FaroErrorBoundaryState { 23 | hasError: boolean; 24 | error: Error | null; 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/src/errorBoundary/withFaroErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import hoistNonReactStatics from 'hoist-non-react-statics'; 2 | import type { ComponentType, FC } from 'react'; 3 | 4 | import { unknownString } from '@grafana/faro-core'; 5 | 6 | import { FaroErrorBoundary } from './FaroErrorBoundary'; 7 | import type { FaroErrorBoundaryProps, ReactProps } from './types'; 8 | 9 | export function withFaroErrorBoundary

( 10 | WrappedComponent: ComponentType

, 11 | errorBoundaryProps: FaroErrorBoundaryProps 12 | ): FC

{ 13 | const componentDisplayName = WrappedComponent.displayName ?? WrappedComponent.name ?? unknownString; 14 | 15 | const Component: FC

= (wrappedComponentProps: P) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | Component.displayName = `faroErrorBoundary(${componentDisplayName})`; 22 | 23 | hoistNonReactStatics(Component, WrappedComponent); 24 | 25 | return Component; 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { BaseInstrumentation, VERSION } from '@grafana/faro-web-sdk'; 2 | 3 | import { setDependencies } from './dependencies'; 4 | import { initializeReactRouterInstrumentation } from './router'; 5 | import type { ReactIntegrationConfig } from './types'; 6 | 7 | export class ReactIntegration extends BaseInstrumentation { 8 | name = '@grafana/faro-react'; 9 | version = VERSION; 10 | 11 | constructor(private options: ReactIntegrationConfig = {}) { 12 | super(); 13 | } 14 | 15 | initialize(): void { 16 | setDependencies(this.internalLogger, this.api); 17 | initializeReactRouterInstrumentation(this.options); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/profiler/index.ts: -------------------------------------------------------------------------------- 1 | export { FaroProfiler } from './FaroProfiler'; 2 | export type { FaroProfilerProps } from './FaroProfiler'; 3 | 4 | export { withFaroProfiler } from './withFaroProfiler'; 5 | -------------------------------------------------------------------------------- /packages/react/src/profiler/withFaroProfiler.tsx: -------------------------------------------------------------------------------- 1 | import hoistNonReactStatics from 'hoist-non-react-statics'; 2 | import type { ComponentType, FC } from 'react'; 3 | 4 | import { unknownString } from '@grafana/faro-core'; 5 | 6 | import { FaroProfiler } from './FaroProfiler'; 7 | import type { FaroProfilerProps } from './FaroProfiler'; 8 | 9 | export function withFaroProfiler

>( 10 | WrappedComponent: ComponentType

, 11 | options?: Omit 12 | ): FC

{ 13 | const componentDisplayName = options?.name ?? WrappedComponent.displayName ?? WrappedComponent.name ?? unknownString; 14 | 15 | const Component: FC

= (props: P) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | Component.displayName = `faroProfiler(${componentDisplayName})`; 22 | 23 | hoistNonReactStatics(Component, WrappedComponent); 24 | 25 | return Component; 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/router/types.ts: -------------------------------------------------------------------------------- 1 | export interface ReactRouterLocation { 2 | hash: string; 3 | key: string; 4 | pathname: string; 5 | search: string; 6 | state: S; 7 | } 8 | 9 | export interface ReactRouterHistory extends Record { 10 | listen?: (cb: (location: ReactRouterLocation, action: NavigationType) => void) => void; 11 | location?: ReactRouterLocation; 12 | } 13 | 14 | export enum ReactRouterVersion { 15 | V4 = 'v4', 16 | V5 = 'v5', 17 | V6 = 'v6', 18 | V6_data_router = 'v6_data_router', 19 | } 20 | 21 | export enum NavigationType { 22 | Pop = 'POP', 23 | Push = 'PUSH', 24 | Replace = 'REPLACE', 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/src/router/v4v5/FaroRoute.tsx: -------------------------------------------------------------------------------- 1 | import { setActiveEventRoute } from './activeEvent'; 2 | import { Route } from './routerDependencies'; 3 | import type { ReactRouterV4V5RouteProps } from './types'; 4 | 5 | export function FaroRoute(props: ReactRouterV4V5RouteProps) { 6 | if (props?.computedMatch?.isExact) { 7 | setActiveEventRoute(props.computedMatch.path); 8 | } 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/src/router/v4v5/activeEvent.ts: -------------------------------------------------------------------------------- 1 | import { EVENT_ROUTE_CHANGE } from '@grafana/faro-web-sdk'; 2 | 3 | import { api } from '../../dependencies'; 4 | 5 | import type { ReactRouterV4V5ActiveEvent } from './types'; 6 | 7 | export let activeEvent: ReactRouterV4V5ActiveEvent | undefined = undefined; 8 | 9 | export function createNewActiveEvent(url: string): ReactRouterV4V5ActiveEvent { 10 | activeEvent = { 11 | route: '', 12 | url, 13 | }; 14 | 15 | return activeEvent; 16 | } 17 | 18 | export function setActiveEventRoute(route: string): void { 19 | if (activeEvent) { 20 | activeEvent.route = route; 21 | } 22 | } 23 | 24 | export function sendActiveEvent(): void { 25 | api.pushEvent(EVENT_ROUTE_CHANGE, activeEvent, undefined, { skipDedupe: true }); 26 | 27 | activeEvent = undefined; 28 | } 29 | -------------------------------------------------------------------------------- /packages/react/src/router/v4v5/index.ts: -------------------------------------------------------------------------------- 1 | export { FaroRoute } from './FaroRoute'; 2 | 3 | export { 4 | createReactRouterV4Options, 5 | createReactRouterV5Options, 6 | initializeReactRouterV4V5Instrumentation, 7 | } from './initialize'; 8 | 9 | export { setReactRouterV4V5SSRDependencies } from './routerDependencies'; 10 | 11 | export type { ReactRouterV4V5ActiveEvent, ReactRouterV4V5Dependencies, ReactRouterV4V5RouteShape } from './types'; 12 | -------------------------------------------------------------------------------- /packages/react/src/router/v4v5/routerDependencies.ts: -------------------------------------------------------------------------------- 1 | import type { ReactRouterHistory } from '../types'; 2 | 3 | import type { ReactRouterV4V5Dependencies, ReactRouterV4V5RouteShape } from './types'; 4 | 5 | export let isInitialized = false; 6 | export let history: ReactRouterHistory; 7 | export let Route: ReactRouterV4V5RouteShape; 8 | 9 | export function setReactRouterV4V5Dependencies(dependencies: ReactRouterV4V5Dependencies): void { 10 | isInitialized = true; 11 | 12 | history = dependencies.history; 13 | Route = dependencies.Route; 14 | } 15 | 16 | export function setReactRouterV4V5SSRDependencies(newDependencies: Pick): void { 17 | Route = newDependencies.Route; 18 | } 19 | -------------------------------------------------------------------------------- /packages/react/src/router/v4v5/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactRouterHistory } from '../types'; 2 | 3 | export type ReactRouterV4V5RouteShape = any; 4 | 5 | export interface ReactRouterV4V5RouteProps extends Record { 6 | computedMatch?: { 7 | isExact: boolean; 8 | path: string; 9 | }; 10 | } 11 | 12 | export interface ReactRouterV4V5Dependencies { 13 | history: ReactRouterHistory; 14 | Route: ReactRouterV4V5RouteShape; 15 | } 16 | 17 | export interface ReactRouterV4V5ActiveEvent extends Record { 18 | route: string; 19 | url: string; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/router/v6/index.ts: -------------------------------------------------------------------------------- 1 | export { FaroRoutes } from './FaroRoutes'; 2 | 3 | export { 4 | createReactRouterV6Options, 5 | createReactRouterV6DataOptions, 6 | initializeReactRouterV6Instrumentation, 7 | initializeReactRouterV6DataRouterInstrumentation, 8 | } from './initialize'; 9 | 10 | export { setReactRouterV6SSRDependencies } from './routerDependencies'; 11 | 12 | export { withFaroRouterInstrumentation } from './withFaroRouterInstrumentation'; 13 | 14 | export type { 15 | ReactRouterV6CreateRoutesFromChildren, 16 | ReactRouterV6Dependencies, 17 | ReactRouterV6MatchRoutes, 18 | ReactRouterV6Params, 19 | ReactRouterV6RouteMatch, 20 | ReactRouterV6RouteObject, 21 | ReactRouterV6RoutesProps, 22 | ReactRouterV6RoutesShape, 23 | ReactRouterV6UseLocation, 24 | ReactRouterV6UseNavigationType, 25 | ReactRouterV6DataRouterDependencies, 26 | } from './types'; 27 | -------------------------------------------------------------------------------- /packages/react/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ReactRouterV4V5Dependencies, 3 | ReactRouterV6DataRouterDependencies, 4 | ReactRouterV6Dependencies, 5 | ReactRouterVersion, 6 | } from './router'; 7 | 8 | export interface ReactRouterV4V5Config { 9 | version: ReactRouterVersion.V4 | ReactRouterVersion.V5; 10 | dependencies: ReactRouterV4V5Dependencies; 11 | } 12 | 13 | export interface ReactRouterV6Config { 14 | version: ReactRouterVersion.V6; 15 | dependencies: ReactRouterV6Dependencies; 16 | } 17 | 18 | export interface ReactRouterV6DataRouterConfig { 19 | version: ReactRouterVersion.V6_data_router; 20 | dependencies: ReactRouterV6DataRouterDependencies; 21 | } 22 | 23 | export interface ReactIntegrationConfig { 24 | router?: ReactRouterV4V5Config | ReactRouterV6Config | ReactRouterV6DataRouterConfig; 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getMajorReactVersion, 3 | isReactVersionAtLeast, 4 | isReactVersionAtLeast16, 5 | isReactVersionAtLeast17, 6 | isReactVersionAtLeast18, 7 | reactVersion, 8 | reactVersionMajor, 9 | } from './reactVersion'; 10 | -------------------------------------------------------------------------------- /packages/react/src/utils/reactVersion.ts: -------------------------------------------------------------------------------- 1 | import { version } from 'react'; 2 | 3 | export const reactVersion = version; 4 | export const reactVersionMajor = getMajorReactVersion(); 5 | export const isReactVersionAtLeast18 = isReactVersionAtLeast(18); 6 | export const isReactVersionAtLeast17 = isReactVersionAtLeast(17); 7 | export const isReactVersionAtLeast16 = isReactVersionAtLeast(16); 8 | 9 | export function getMajorReactVersion(): number | null { 10 | const major = reactVersion.split('.'); 11 | 12 | try { 13 | return major[0] ? parseInt(major[0], 10) : null; 14 | } catch (err) { 15 | return null; 16 | } 17 | } 18 | 19 | export function isReactVersionAtLeast(version: number): boolean { 20 | return reactVersionMajor === null ? false : reactVersionMajor >= version; 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/react.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [ 12 | { "path": "../core/tsconfig.cjs.json" }, 13 | { "path": "../web-sdk/tsconfig.cjs.json" }, 14 | { "path": "../web-tracing/tsconfig.cjs.json" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/react.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [ 12 | { "path": "../core/tsconfig.esm.json" }, 13 | { "path": "../web-sdk/tsconfig.esm.json" }, 14 | { "path": "../web-tracing/tsconfig.esm.json" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "..", 7 | "tsBuildInfoFile": "../../.cache/tsc/react.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [ 11 | { "path": "../core/tsconfig.spec.json" }, 12 | { "path": "../web-sdk/tsconfig.spec.json" }, 13 | { "path": "../web-tracing/tsconfig.spec.json" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/web-sdk/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /packages/web-sdk/globals.ts: -------------------------------------------------------------------------------- 1 | import type { Faro } from '@grafana/faro-core'; 2 | 3 | declare global { 4 | interface Window { 5 | faro: Faro; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/web-sdk/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['packages/web-sdk/src'], 6 | testEnvironment: 'jsdom', 7 | setupFiles: ['/packages/web-sdk/setup.jest.ts'], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/web-sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('webSdk'); 4 | -------------------------------------------------------------------------------- /packages/web-sdk/setup.jest.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder, TextEncoder } from 'node:util'; 2 | 3 | Object.assign(global, { TextEncoder, TextDecoder }); 4 | -------------------------------------------------------------------------------- /packages/web-sdk/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { getWebInstrumentations } from './getWebInstrumentations'; 2 | 3 | export { makeCoreConfig } from './makeCoreConfig'; 4 | 5 | export type { BrowserConfig, GetWebInstrumentationsOptions } from './types'; 6 | -------------------------------------------------------------------------------- /packages/web-sdk/src/config/types.ts: -------------------------------------------------------------------------------- 1 | import type { Config, LogLevel } from '@grafana/faro-core'; 2 | 3 | export interface BrowserConfig extends Partial>, Pick { 4 | url?: string; 5 | apiKey?: string; 6 | } 7 | 8 | export interface GetWebInstrumentationsOptions { 9 | captureConsole?: boolean; 10 | captureConsoleDisabledLevels?: LogLevel[]; 11 | enablePerformanceInstrumentation?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-sdk/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const defaultEventDomain = 'browser'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/initialize.ts: -------------------------------------------------------------------------------- 1 | import { initializeFaro as coreInit } from '@grafana/faro-core'; 2 | import type { Faro } from '@grafana/faro-core'; 3 | 4 | import { makeCoreConfig } from './config'; 5 | import type { BrowserConfig } from './config'; 6 | 7 | export function initializeFaro(config: BrowserConfig): Faro { 8 | const coreConfig = makeCoreConfig(config); 9 | 10 | if (!coreConfig) { 11 | return undefined!; 12 | } 13 | 14 | return coreInit(coreConfig); 15 | } 16 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/console/index.ts: -------------------------------------------------------------------------------- 1 | export { ConsoleInstrumentation } from './instrumentation'; 2 | 3 | export type { ConsoleInstrumentationOptions } from './types'; 4 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/console/types.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@grafana/faro-core'; 2 | 3 | /** 4 | * @deprecated Configure console instrumentation using the `consoleInstrumentation` object in the 5 | * Faro config. 6 | */ 7 | export type ConsoleInstrumentationOptions = Config['consoleInstrumentation']; 8 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/const.ts: -------------------------------------------------------------------------------- 1 | export const primitiveUnhandledValue = 'Non-Error promise rejection captured with value:'; 2 | export const primitiveUnhandledType = 'UnhandledRejection'; 3 | 4 | export const domErrorType = 'DOMError'; 5 | export const domExceptionType = 'DOMException'; 6 | 7 | export const objectEventValue = 'Non-Error exception captured with keys:'; 8 | 9 | export const unknownSymbolString = '?'; 10 | 11 | export const valueTypeRegex = 12 | /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i; 13 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/getValueAndTypeFromMessage.ts: -------------------------------------------------------------------------------- 1 | import { defaultExceptionType } from '@grafana/faro-core'; 2 | 3 | import { valueTypeRegex } from './const'; 4 | 5 | export function getValueAndTypeFromMessage(message: string): [string, string] { 6 | const groups = message.match(valueTypeRegex); 7 | 8 | const type = groups?.[1] ?? defaultExceptionType; 9 | const value = groups?.[2] ?? message; 10 | 11 | return [value, type]; 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorsInstrumentation } from './instrumentation'; 2 | 3 | export { buildStackFrame, getDataFromSafariExtensions, getStackFramesFromError, parseStacktrace } from './stackFrames'; 4 | 5 | export type { ErrorEvent, ExtendedPromiseRejectionEvent } from './types'; 6 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { BaseInstrumentation, VERSION } from '@grafana/faro-core'; 2 | 3 | import { registerOnerror } from './registerOnerror'; 4 | import { registerOnunhandledrejection } from './registerOnunhandledrejection'; 5 | 6 | export class ErrorsInstrumentation extends BaseInstrumentation { 7 | readonly name = '@grafana/faro-web-sdk:instrumentation-errors'; 8 | readonly version = VERSION; 9 | 10 | initialize(): void { 11 | this.logDebug('Initializing'); 12 | 13 | registerOnerror(this.api); 14 | 15 | registerOnunhandledrejection(this.api); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/registerOnerror.ts: -------------------------------------------------------------------------------- 1 | import type { API, PushErrorOptions } from '@grafana/faro-core'; 2 | 3 | import { getDetailsFromErrorArgs } from './getErrorDetails'; 4 | 5 | export function registerOnerror(api: API): void { 6 | const oldOnerror = window.onerror; 7 | 8 | window.onerror = (...args) => { 9 | try { 10 | const { value, type, stackFrames } = getDetailsFromErrorArgs(args); 11 | const originalError = args[4]; 12 | 13 | if (value) { 14 | const options: PushErrorOptions = { type, stackFrames }; 15 | 16 | if (originalError != null) { 17 | options.originalError = originalError; 18 | } 19 | 20 | api.pushError(new Error(value), options); 21 | } 22 | } finally { 23 | oldOnerror?.apply(window, args); 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/stackFrames/buildStackFrame.ts: -------------------------------------------------------------------------------- 1 | import type { ExceptionStackFrame } from '@grafana/faro-core'; 2 | 3 | import { unknownSymbolString } from './const'; 4 | 5 | export function buildStackFrame( 6 | filename: string | undefined, 7 | func: string | undefined, 8 | lineno: number | undefined, 9 | colno: number | undefined 10 | ): ExceptionStackFrame { 11 | const stackFrame: ExceptionStackFrame = { 12 | filename: filename || document.location.href, 13 | function: func || unknownSymbolString, 14 | }; 15 | 16 | if (lineno !== undefined) { 17 | stackFrame.lineno = lineno; 18 | } 19 | 20 | if (colno !== undefined) { 21 | stackFrame.colno = colno; 22 | } 23 | 24 | return stackFrame; 25 | } 26 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/stackFrames/getDataFromSafariExtensions.ts: -------------------------------------------------------------------------------- 1 | import { atString, safariExtensionString, safariWebExtensionString } from './const'; 2 | 3 | export function getDataFromSafariExtensions( 4 | func: string | undefined, 5 | filename: string | undefined 6 | ): [string | undefined, string | undefined] { 7 | const isSafariExtension = func?.includes(safariExtensionString); 8 | const isSafariWebExtension = !isSafariExtension && func?.includes(safariWebExtensionString); 9 | 10 | if (!isSafariExtension && !isSafariWebExtension) { 11 | return [func, filename]; 12 | } 13 | 14 | return [ 15 | func?.includes(atString) ? func.split(atString)[0] : func, 16 | isSafariExtension ? `${safariExtensionString}:${filename}` : `${safariWebExtensionString}:${filename}`, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/stackFrames/index.ts: -------------------------------------------------------------------------------- 1 | export { buildStackFrame } from './buildStackFrame'; 2 | 3 | export { getDataFromSafariExtensions } from './getDataFromSafariExtensions'; 4 | 5 | export { getStackFramesFromError } from './getStackFramesFromError'; 6 | 7 | export { parseStacktrace } from './parseStacktrace'; 8 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/stackFrames/parseStacktrace.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedError, Stacktrace } from '@grafana/faro-core'; 2 | 3 | import { getStackFramesFromError } from './getStackFramesFromError'; 4 | 5 | export function parseStacktrace(error: ExtendedError): Stacktrace { 6 | return { 7 | frames: getStackFramesFromError(error), 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/errors/types.ts: -------------------------------------------------------------------------------- 1 | export interface ExtendedPromiseRejectionEvent extends PromiseRejectionEvent { 2 | detail?: { 3 | reason: PromiseRejectionEvent['reason']; 4 | }; 5 | } 6 | 7 | export type ErrorEvent = (Error | Event) & { 8 | error?: Error; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/index.ts: -------------------------------------------------------------------------------- 1 | export { SessionInstrumentation } from './session'; 2 | 3 | export { ConsoleInstrumentation } from './console'; 4 | export type { ConsoleInstrumentationOptions } from './console'; 5 | 6 | export { 7 | buildStackFrame, 8 | ErrorsInstrumentation, 9 | getDataFromSafariExtensions, 10 | getStackFramesFromError, 11 | parseStacktrace, 12 | } from './errors'; 13 | export type { ErrorEvent, ExtendedPromiseRejectionEvent } from './errors'; 14 | 15 | export { ViewInstrumentation } from './view'; 16 | 17 | export { WebVitalsInstrumentation } from './webVitals'; 18 | 19 | export { 20 | PersistentSessionsManager, 21 | VolatileSessionsManager, 22 | MAX_SESSION_PERSISTENCE_TIME, 23 | MAX_SESSION_PERSISTENCE_TIME_BUFFER, 24 | SESSION_EXPIRATION_TIME, 25 | SESSION_INACTIVITY_TIME, 26 | STORAGE_KEY, 27 | } from './session'; 28 | 29 | export { PerformanceInstrumentation } from './performance'; 30 | 31 | export { UserActionInstrumentation, userActionDataAttribute, startUserAction } from './userActions'; 32 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/instrumentationConstants.ts: -------------------------------------------------------------------------------- 1 | export const NAVIGATION_ID_STORAGE_KEY = 'com.grafana.faro.lastNavigationId'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/performance/index.ts: -------------------------------------------------------------------------------- 1 | export { PerformanceInstrumentation } from './instrumentation'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/performance/performanceConstants.ts: -------------------------------------------------------------------------------- 1 | export const NAVIGATION_ENTRY = 'navigation'; 2 | export const RESOURCE_ENTRY = 'resource'; 3 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/index.ts: -------------------------------------------------------------------------------- 1 | export { SessionInstrumentation } from './instrumentation'; 2 | 3 | export { 4 | MAX_SESSION_PERSISTENCE_TIME, 5 | MAX_SESSION_PERSISTENCE_TIME_BUFFER, 6 | PersistentSessionsManager, 7 | SESSION_EXPIRATION_TIME, 8 | SESSION_INACTIVITY_TIME, 9 | STORAGE_KEY, 10 | STORAGE_UPDATE_DELAY, 11 | VolatileSessionsManager, 12 | defaultSessionTrackingConfig, 13 | isSampled, 14 | } from './sessionManager'; 15 | 16 | export type { FaroUserSession } from './sessionManager'; 17 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/sessionManager/getSessionManagerByConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@grafana/faro-core'; 2 | 3 | import { PersistentSessionsManager } from './PersistentSessionsManager'; 4 | import type { SessionManager } from './types'; 5 | import { VolatileSessionsManager } from './VolatileSessionManager'; 6 | 7 | export function getSessionManagerByConfig(sessionTrackingConfig: Config['sessionTracking']): SessionManager { 8 | return sessionTrackingConfig?.persistent ? PersistentSessionsManager : VolatileSessionsManager; 9 | } 10 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/sessionManager/index.ts: -------------------------------------------------------------------------------- 1 | export { PersistentSessionsManager } from './PersistentSessionsManager'; 2 | export { VolatileSessionsManager } from './VolatileSessionManager'; 3 | 4 | export { 5 | MAX_SESSION_PERSISTENCE_TIME, 6 | MAX_SESSION_PERSISTENCE_TIME_BUFFER, 7 | SESSION_EXPIRATION_TIME, 8 | SESSION_INACTIVITY_TIME, 9 | STORAGE_KEY, 10 | STORAGE_UPDATE_DELAY, 11 | defaultSessionTrackingConfig, 12 | } from './sessionConstants'; 13 | 14 | export { isSampled } from './sampling'; 15 | 16 | export type { FaroUserSession } from './types'; 17 | 18 | export { getSessionManagerByConfig } from './getSessionManagerByConfig'; 19 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/sessionManager/sampling.ts: -------------------------------------------------------------------------------- 1 | import { faro } from '@grafana/faro-core'; 2 | 3 | export function isSampled(): boolean { 4 | const sendAllSignals = 1; 5 | const sessionTracking = faro.config.sessionTracking; 6 | let samplingRate = 7 | sessionTracking?.sampler?.({ metas: faro.metas.value }) ?? sessionTracking?.samplingRate ?? sendAllSignals; 8 | 9 | if (typeof samplingRate !== 'number') { 10 | const sendNoSignals = 0; 11 | samplingRate = sendNoSignals; 12 | } 13 | 14 | return Math.random() < samplingRate; 15 | } 16 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/sessionManager/sessionConstants.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@grafana/faro-core'; 2 | 3 | export const STORAGE_KEY = 'com.grafana.faro.session'; 4 | export const SESSION_EXPIRATION_TIME = 4 * 60 * 60 * 1000; // hrs 5 | export const SESSION_INACTIVITY_TIME = 15 * 60 * 1000; // minutes 6 | export const STORAGE_UPDATE_DELAY = 1 * 1000; // seconds 7 | 8 | /** 9 | * @deprecated MAX_SESSION_PERSISTENCE_TIME_BUFFER is not used anymore. The constant will be removed in the future 10 | */ 11 | export const MAX_SESSION_PERSISTENCE_TIME_BUFFER = 1 * 60 * 1000; 12 | export const MAX_SESSION_PERSISTENCE_TIME = SESSION_INACTIVITY_TIME; 13 | 14 | export const defaultSessionTrackingConfig: Config['sessionTracking'] = { 15 | enabled: true, 16 | persistent: false, 17 | maxSessionPersistenceTime: MAX_SESSION_PERSISTENCE_TIME, 18 | } as const; 19 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/session/sessionManager/types.ts: -------------------------------------------------------------------------------- 1 | import type { MetaSession } from '@grafana/faro-core'; 2 | 3 | import type { PersistentSessionsManager } from './PersistentSessionsManager'; 4 | import type { VolatileSessionsManager } from './VolatileSessionManager'; 5 | 6 | export interface FaroUserSession { 7 | sessionId: string; 8 | lastActivity: number; 9 | started: number; 10 | isSampled: boolean; 11 | sessionMeta?: MetaSession; 12 | } 13 | 14 | export type SessionManager = typeof VolatileSessionsManager | typeof PersistentSessionsManager; 15 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/const.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGE_TYPE_RESOURCE_ENTRY = 'resource-entry'; 2 | export const MESSAGE_TYPE_HTTP_REQUEST_START = 'http-request-start'; 3 | export const MESSAGE_TYPE_HTTP_REQUEST_END = 'http-request-end'; 4 | export const MESSAGE_TYPE_DOM_MUTATION = 'dom-mutation'; 5 | 6 | export const userActionDataAttributeParsed = 'faroUserActionName'; 7 | export const userActionDataAttribute = 'data-faro-user-action-name'; 8 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/domMutationMonitor.test.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | 3 | import { MESSAGE_TYPE_DOM_MUTATION } from './const'; 4 | import { monitorDomMutations } from './domMutationMonitor'; 5 | 6 | describe('DOM Mutation Monitor', () => { 7 | it('MutationObserver takeRecords method', () => { 8 | // Set up a basic DOM using JSDOM 9 | const dom = new JSDOM(`

Hello

`); 10 | const document = dom.window.document; 11 | const targetNode = document.getElementById('test'); 12 | 13 | const observable = monitorDomMutations(); 14 | 15 | // Simulate a DOM change 16 | targetNode?.setAttribute('data-test', 'value'); 17 | observable.subscribe((msg) => { 18 | expect(msg).toEqual({ type: MESSAGE_TYPE_DOM_MUTATION }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/domMutationMonitor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from '@grafana/faro-core'; 2 | 3 | import { MESSAGE_TYPE_DOM_MUTATION } from './const'; 4 | import type { DomMutationMessage } from './types'; 5 | 6 | export function monitorDomMutations(): Observable { 7 | const observable = new Observable(); 8 | 9 | const observer = new MutationObserver((_mutationsList, _observer) => { 10 | observable.notify({ type: MESSAGE_TYPE_DOM_MUTATION }); 11 | }); 12 | 13 | observer.observe(document, { 14 | attributes: true, 15 | childList: true, 16 | subtree: true, 17 | characterData: true, 18 | }); 19 | 20 | return observable; 21 | } 22 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/index.ts: -------------------------------------------------------------------------------- 1 | export { UserActionInstrumentation, startUserAction } from './instrumentation'; 2 | 3 | export type { 4 | DomMutationMessage, 5 | HttpRequestEndMessage, 6 | HttpRequestStartMessage, 7 | HttpRequestMessagePayload, 8 | } from './types'; 9 | 10 | export { 11 | MESSAGE_TYPE_DOM_MUTATION, 12 | MESSAGE_TYPE_HTTP_REQUEST_END, 13 | MESSAGE_TYPE_HTTP_REQUEST_START, 14 | userActionDataAttribute, 15 | } from './const'; 16 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/performanceEntriesMonitor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from '@grafana/faro-core'; 2 | 3 | import { performanceEntriesSubscription } from '../performance/instrumentation'; 4 | import { RESOURCE_ENTRY } from '../performance/performanceConstants'; 5 | 6 | import { MESSAGE_TYPE_RESOURCE_ENTRY } from './const'; 7 | 8 | export function monitorPerformanceEntries(): Observable { 9 | const observable = new Observable(); 10 | 11 | performanceEntriesSubscription.subscribe((data) => { 12 | if (data.type === RESOURCE_ENTRY) { 13 | observable.notify({ type: MESSAGE_TYPE_RESOURCE_ENTRY }); 14 | } 15 | }); 16 | 17 | return observable; 18 | } 19 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MESSAGE_TYPE_DOM_MUTATION, 3 | MESSAGE_TYPE_HTTP_REQUEST_END, 4 | MESSAGE_TYPE_HTTP_REQUEST_START, 5 | } from './const'; 6 | 7 | export type DomMutationMessage = { 8 | type: typeof MESSAGE_TYPE_DOM_MUTATION; 9 | }; 10 | 11 | type RequestApiType = 'xhr' | 'fetch'; 12 | 13 | export type HttpRequestMessagePayload = { 14 | requestId: string; 15 | url: string; 16 | method: string; 17 | apiType: RequestApiType; 18 | }; 19 | 20 | export type HttpRequestStartMessage = { 21 | type: typeof MESSAGE_TYPE_HTTP_REQUEST_START; 22 | request: HttpRequestMessagePayload; 23 | }; 24 | 25 | export type HttpRequestEndMessage = { 26 | type: typeof MESSAGE_TYPE_HTTP_REQUEST_END; 27 | request: HttpRequestMessagePayload; 28 | }; 29 | 30 | export type ApiEvent = { 31 | name: string; 32 | attributes?: Record; 33 | type: 'apiEvent'; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/util.test.ts: -------------------------------------------------------------------------------- 1 | import { convertDataAttributeName } from './util'; 2 | 3 | describe('util', () => { 4 | it('converts data attribute to camelCase and remove the "data-" prefix', () => { 5 | expect(convertDataAttributeName('data-test-action-name')).toBe('testActionName'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/userActions/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses the action attribute name by removing the 'data-' prefix and converting 3 | * the remaining string to camelCase. 4 | * 5 | * This is needed because the browser will remove the 'data-' prefix and the dashes from 6 | * data attributes and make then camelCase. 7 | */ 8 | export function convertDataAttributeName(userActionDataAttribute: string) { 9 | const withoutData = userActionDataAttribute.split('data-')[1]; 10 | const withUpperCase = withoutData?.replace(/-(.)/g, (_, char) => char.toUpperCase()); 11 | return withUpperCase?.replace(/-/g, ''); 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/view/index.ts: -------------------------------------------------------------------------------- 1 | export { ViewInstrumentation } from './instrumentation'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/view/instrumentation.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEvent, initializeFaro, TransportItem } from '@grafana/faro-core'; 2 | import { mockConfig, MockTransport } from '@grafana/faro-core/src/testUtils'; 3 | 4 | import { ViewInstrumentation } from './instrumentation'; 5 | 6 | describe('ViewInstrumentation', () => { 7 | it('will send view changed event if setView is called.', () => { 8 | const transport = new MockTransport(); 9 | const view = { name: 'my-view' }; 10 | 11 | const { api } = initializeFaro( 12 | mockConfig({ 13 | transports: [transport], 14 | instrumentations: [new ViewInstrumentation()], 15 | view, 16 | }) 17 | ); 18 | 19 | const newView = { name: 'my-view' }; 20 | api.setView(newView); 21 | expect(transport.items).toHaveLength(1); 22 | 23 | let event = transport.items[0]! as TransportItem; 24 | expect(event.meta.view?.name).toEqual(view.name); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/webVitals/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsInstrumentation } from './instrumentation'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/webVitals/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { BaseInstrumentation, VERSION } from '@grafana/faro-core'; 2 | 3 | import { WebVitalsBasic } from './webVitalsBasic'; 4 | import { WebVitalsWithAttribution } from './webVitalsWithAttribution'; 5 | 6 | export class WebVitalsInstrumentation extends BaseInstrumentation { 7 | readonly name = '@grafana/faro-web-sdk:instrumentation-web-vitals'; 8 | readonly version = VERSION; 9 | 10 | initialize(): void { 11 | this.logDebug('Initializing'); 12 | const webVitals = this.intializeWebVitalsInstrumentation(); 13 | webVitals.initialize(); 14 | } 15 | 16 | private intializeWebVitalsInstrumentation() { 17 | if ( 18 | this.config?.trackWebVitalsAttribution === false || 19 | this.config?.webVitalsInstrumentation?.trackAttribution === false 20 | ) { 21 | return new WebVitalsBasic(this.api.pushMeasurement, this.config.webVitalsInstrumentation); 22 | } 23 | return new WebVitalsWithAttribution(this.api.pushMeasurement, this.config.webVitalsInstrumentation); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/web-sdk/src/instrumentations/webVitals/webVitalsBasic.ts: -------------------------------------------------------------------------------- 1 | import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals'; 2 | 3 | import type { Config, MeasurementsAPI } from '@grafana/faro-core'; 4 | 5 | export class WebVitalsBasic { 6 | static mapping = { 7 | cls: onCLS, 8 | fcp: onFCP, 9 | fid: onFID, 10 | inp: onINP, 11 | lcp: onLCP, 12 | ttfb: onTTFB, 13 | }; 14 | 15 | constructor( 16 | private pushMeasurement: MeasurementsAPI['pushMeasurement'], 17 | private webVitalConfig?: Config['webVitalsInstrumentation'] 18 | ) {} 19 | 20 | initialize(): void { 21 | Object.entries(WebVitalsBasic.mapping).forEach(([indicator, executor]) => { 22 | executor( 23 | (metric) => { 24 | this.pushMeasurement({ 25 | type: 'web-vitals', 26 | 27 | values: { 28 | [indicator]: metric.value, 29 | }, 30 | }); 31 | }, 32 | { reportAllChanges: this.webVitalConfig?.reportAllChanges } 33 | ); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/browser/index.ts: -------------------------------------------------------------------------------- 1 | export { browserMeta } from './meta'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/index.ts: -------------------------------------------------------------------------------- 1 | export { browserMeta } from './browser'; 2 | 3 | export { createSession } from './session'; 4 | 5 | export { sdkMeta } from './sdk'; 6 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/k6/index.ts: -------------------------------------------------------------------------------- 1 | export { k6Meta } from './meta'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/k6/meta.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, MetaItem } from '@grafana/faro-core'; 2 | 3 | type K6Properties = { 4 | testRunId?: string; 5 | }; 6 | 7 | export const k6Meta: MetaItem> = () => { 8 | const k6Properties: K6Properties = (window as any).k6; 9 | 10 | return { 11 | k6: { 12 | // we only add the k6 meta if Faro is running inside a k6 environment, so this is always true 13 | isK6Browser: true, 14 | ...(k6Properties?.testRunId && { testRunId: k6Properties?.testRunId }), 15 | }, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/page/index.ts: -------------------------------------------------------------------------------- 1 | export { createPageMeta } from './meta'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/page/meta.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, type Meta, type MetaItem } from '@grafana/faro-core'; 2 | 3 | let currentHref: string | undefined; 4 | let pageId: string | undefined; 5 | 6 | type createPageMetaProps = { 7 | generatePageId?: (location: Location) => string; 8 | initialPageMeta?: Meta['page']; 9 | }; 10 | 11 | export function createPageMeta({ generatePageId, initialPageMeta }: createPageMetaProps = {}): MetaItem< 12 | Pick 13 | > { 14 | const pageMeta: MetaItem> = () => { 15 | const locationHref = location.href; 16 | 17 | if (isFunction(generatePageId) && currentHref !== locationHref) { 18 | currentHref = locationHref; 19 | pageId = generatePageId(location); 20 | } 21 | 22 | return { 23 | page: { 24 | url: locationHref, 25 | ...(pageId ? { id: pageId } : {}), 26 | ...initialPageMeta, 27 | }, 28 | }; 29 | }; 30 | 31 | return pageMeta; 32 | } 33 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export { sdkMeta } from './meta'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/sdk/meta.ts: -------------------------------------------------------------------------------- 1 | import { faro, VERSION } from '@grafana/faro-core'; 2 | import type { Meta, MetaItem } from '@grafana/faro-core'; 3 | 4 | export const sdkMeta: MetaItem> = () => ({ 5 | sdk: { 6 | name: '@grafana/faro-core', 7 | version: VERSION, 8 | integrations: faro.config.instrumentations.map(({ name, version }) => ({ name, version })), 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/session/createSession.ts: -------------------------------------------------------------------------------- 1 | import { faro, genShortID } from '@grafana/faro-core'; 2 | import type { MetaSession } from '@grafana/faro-core'; 3 | 4 | export function createSession(attributes?: MetaSession['attributes']): MetaSession { 5 | return { 6 | id: faro.config?.sessionTracking?.generateSessionId?.() ?? genShortID(), 7 | attributes, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/web-sdk/src/metas/session/index.ts: -------------------------------------------------------------------------------- 1 | export { createSession } from './createSession'; 2 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/console/index.ts: -------------------------------------------------------------------------------- 1 | export { ConsoleTransport } from './transport'; 2 | export type { ConsoleTransportOptions } from './types'; 3 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/console/transport.ts: -------------------------------------------------------------------------------- 1 | import { BaseTransport, getTransportBody, LogLevel, VERSION } from '@grafana/faro-core'; 2 | import type { TransportItem } from '@grafana/faro-core'; 3 | 4 | import type { ConsoleTransportOptions } from './types'; 5 | 6 | export class ConsoleTransport extends BaseTransport { 7 | readonly name = '@grafana/faro-web-sdk:transport-console'; 8 | readonly version = VERSION; 9 | 10 | constructor(private options: ConsoleTransportOptions = {}) { 11 | super(); 12 | } 13 | 14 | send(item: TransportItem): void { 15 | return this.unpatchedConsole[this.options.level ?? LogLevel.DEBUG]('New event', getTransportBody([item])); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/console/types.ts: -------------------------------------------------------------------------------- 1 | import type { LogLevel } from '@grafana/faro-core'; 2 | 3 | export interface ConsoleTransportOptions { 4 | level?: LogLevel; 5 | } 6 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export { FetchTransport } from './transport'; 2 | export type { ClockFn, FetchTransportOptions, FetchTransportRequestOptions } from './types'; 3 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/fetch/types.ts: -------------------------------------------------------------------------------- 1 | export interface FetchTransportRequestOptions extends Omit { 2 | headers?: Record; 3 | } 4 | 5 | export interface FetchTransportOptions { 6 | // url of the collector endpoint 7 | url: string; 8 | 9 | // will be added as `x-api-key` header 10 | apiKey?: string; 11 | // how many requests to buffer in total 12 | bufferSize?: number; 13 | // how many requests to execute concurrently 14 | concurrency?: number; 15 | // if rate limit response does not include a Retry-After header, 16 | // how many milliseconds to back off before attempting a request. 17 | // intermediate events will be dropped, not buffered 18 | defaultRateLimitBackoffMs?: number; 19 | // get current date. for mocking purposes in tests 20 | getNow?: ClockFn; 21 | // addition options for global.Fetch 22 | requestOptions?: FetchTransportRequestOptions; 23 | } 24 | 25 | export type ClockFn = () => number; 26 | -------------------------------------------------------------------------------- /packages/web-sdk/src/transports/index.ts: -------------------------------------------------------------------------------- 1 | export { ConsoleTransport } from './console'; 2 | export type { ConsoleTransportOptions } from './console'; 3 | 4 | export { FetchTransport } from './fetch'; 5 | export type { ClockFn, FetchTransportOptions, FetchTransportRequestOptions } from './fetch'; 6 | -------------------------------------------------------------------------------- /packages/web-sdk/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getItem, 3 | isLocalStorageAvailable, 4 | isSessionStorageAvailable, 5 | isWebStorageAvailable, 6 | removeItem, 7 | setItem, 8 | webStorageType, 9 | } from './webStorage'; 10 | 11 | export { throttle } from './throttle'; 12 | 13 | export { getIgnoreUrls, getUrlFromResource } from './url'; 14 | -------------------------------------------------------------------------------- /packages/web-sdk/src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tail based throttle which caches the args of the last call and updates 3 | */ 4 | export function throttle void>(callback: T, delay: number) { 5 | let pause = false; 6 | let lastPending: Parameters | null; 7 | 8 | const timeoutBehavior = () => { 9 | if (lastPending == null) { 10 | pause = false; 11 | return; 12 | } 13 | 14 | callback(...lastPending); 15 | lastPending = null; 16 | setTimeout(timeoutBehavior, delay); 17 | }; 18 | 19 | return (...args: Parameters) => { 20 | if (pause) { 21 | lastPending = args; 22 | return; 23 | } 24 | 25 | callback(...args); 26 | pause = true; 27 | setTimeout(timeoutBehavior, delay); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/web-sdk/src/utils/webStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { isWebStorageAvailable } from './webStorage'; 2 | 3 | let windowSpy: jest.SpyInstance; 4 | 5 | describe('webStorage', () => { 6 | beforeEach(() => { 7 | windowSpy = jest.spyOn(globalThis, 'window', 'get'); 8 | }); 9 | 10 | afterEach(() => { 11 | windowSpy.mockRestore(); 12 | }); 13 | 14 | it('Returns true if local storage is available.', () => { 15 | const localStorageAvailable = isWebStorageAvailable('localStorage'); 16 | expect(localStorageAvailable).toBe(true); 17 | }); 18 | 19 | it('Returns false if local storage is not available.', () => { 20 | disableLocalStorage(); 21 | const localStorageAvailable = isWebStorageAvailable('localStorage'); 22 | expect(localStorageAvailable).toBe(false); 23 | }); 24 | }); 25 | 26 | function disableLocalStorage() { 27 | windowSpy.mockImplementation(() => ({ 28 | localStorage: { 29 | setItem() { 30 | throw new Error(); 31 | }, 32 | }, 33 | })); 34 | } 35 | -------------------------------------------------------------------------------- /packages/web-sdk/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/web-sdk/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/webSdk.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../core/tsconfig.cjs.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-sdk/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/webSdk.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../core/tsconfig.esm.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/web-sdk/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "..", 7 | "tsBuildInfoFile": "../../.cache/tsc/webSdk.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /packages/web-tracing/.browserslistrc: -------------------------------------------------------------------------------- 1 | supports es6-module -------------------------------------------------------------------------------- /packages/web-tracing/README.md: -------------------------------------------------------------------------------- 1 | # @grafana/faro-web-tracing 2 | 3 | This package provides tools for integrating [OpenTelemetry][opentelemetry-js] based tracing with the 4 | [Faro for the web][faro-web-sdk-package]. 5 | 6 | See [quick start document][quick-start] for instructions how to set up and use. 7 | 8 | [faro-web-sdk-package]: https://github.com/grafana/faro-web-sdk/tree/main/packages/web-sdk 9 | [opentelemetry-js]: https://opentelemetry.io/docs/instrumentation/js/ 10 | [quick-start]: https://github.com/grafana/faro-web-sdk/blob/main/docs/sources/tutorials/quick-start-browser.md 11 | -------------------------------------------------------------------------------- /packages/web-tracing/jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestBaseConfig } = require('../../jest.config.base.js'); 2 | 3 | module.exports = { 4 | ...jestBaseConfig, 5 | roots: ['packages/web-tracing/src'], 6 | testEnvironment: 'jsdom', 7 | setupFiles: ['/packages/web-tracing/setup.jest.ts'], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/web-tracing/rollup.config.js: -------------------------------------------------------------------------------- 1 | const { getRollupConfigBase } = require('../../rollup.config.base.js'); 2 | 3 | module.exports = getRollupConfigBase('webTracing'); 4 | -------------------------------------------------------------------------------- /packages/web-tracing/setup.jest.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder, TextEncoder } from 'node:util'; 2 | 3 | Object.assign(global, { TextEncoder, TextDecoder }); 4 | -------------------------------------------------------------------------------- /packages/web-tracing/src/faroTraceExporter.ts: -------------------------------------------------------------------------------- 1 | import { ExportResultCode } from '@opentelemetry/core'; 2 | import type { ExportResult } from '@opentelemetry/core'; 3 | import { createExportTraceServiceRequest } from '@opentelemetry/otlp-transformer/build/src/trace/internal'; 4 | import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-web'; 5 | 6 | import { sendFaroEvents } from './faroTraceExporter.utils'; 7 | import type { FaroTraceExporterConfig } from './types'; 8 | 9 | export class FaroTraceExporter implements SpanExporter { 10 | constructor(private config: FaroTraceExporterConfig) {} 11 | 12 | export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void { 13 | const traceEvent = createExportTraceServiceRequest(spans, { useHex: true, useLongBits: false }); 14 | 15 | this.config.api.pushTraces(traceEvent); 16 | sendFaroEvents(traceEvent.resourceSpans); 17 | 18 | resultCallback({ code: ExportResultCode.SUCCESS }); 19 | } 20 | 21 | shutdown(): Promise { 22 | return Promise.resolve(undefined); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/web-tracing/src/index.ts: -------------------------------------------------------------------------------- 1 | export { FaroTraceExporter } from './faroTraceExporter'; 2 | 3 | export { FaroSessionSpanProcessor } from './sessionSpanProcessor'; 4 | 5 | export { getDefaultOTELInstrumentations } from './getDefaultOTELInstrumentations'; 6 | 7 | export { TracingInstrumentation } from './instrumentation'; 8 | 9 | export { getSamplingDecision } from './sampler'; 10 | 11 | export type { FaroTraceExporterConfig, TracingInstrumentationOptions } from './types'; 12 | 13 | export { setSpanStatusOnFetchError, fetchCustomAttributeFunctionWithDefaults } from './instrumentationUtils'; 14 | -------------------------------------------------------------------------------- /packages/web-tracing/src/sampler.test.ts: -------------------------------------------------------------------------------- 1 | import { SamplingDecision } from '@opentelemetry/sdk-trace-web'; 2 | 3 | import { getSamplingDecision } from './sampler'; 4 | 5 | describe('Sampler', () => { 6 | afterEach(() => { 7 | jest.restoreAllMocks(); 8 | }); 9 | 10 | it('Set SamplingDecision to NOT_RECORD (0) if session is not part of the sample', () => { 11 | const samplingDecision = getSamplingDecision({ 12 | attributes: { 13 | isSampled: 'false', 14 | }, 15 | }); 16 | 17 | expect(samplingDecision).toBe(SamplingDecision.NOT_RECORD); 18 | }); 19 | 20 | it('Set SamplingDecision to RECORD_AND_SAMPLED (2) if session is part of the sample', () => { 21 | const samplingDecision = getSamplingDecision({ 22 | attributes: { 23 | isSampled: 'true', 24 | }, 25 | }); 26 | 27 | expect(samplingDecision).toBe(SamplingDecision.RECORD_AND_SAMPLED); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/web-tracing/src/sampler.ts: -------------------------------------------------------------------------------- 1 | import { SamplingDecision } from '@opentelemetry/sdk-trace-web'; 2 | 3 | import type { MetaSession } from '@grafana/faro-web-sdk'; 4 | 5 | export function getSamplingDecision(sessionMeta: MetaSession = {}): SamplingDecision { 6 | const isSessionSampled = sessionMeta.attributes?.['isSampled'] === 'true'; 7 | const samplingDecision = isSessionSampled ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD; 8 | 9 | return samplingDecision; 10 | } 11 | -------------------------------------------------------------------------------- /packages/web-tracing/tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/bundle/types", 5 | "outDir": "./dist/bundle", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/core.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["./**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/web-tracing/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/webTracing.cjs.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../core/tsconfig.cjs.json" }, { "path": "../web-sdk/tsconfig.cjs.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-tracing/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.esm.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "tsBuildInfoFile": "../../.cache/tsc/webTracing.esm.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["**/*.test.ts"], 11 | "references": [{ "path": "../core/tsconfig.esm.json" }, { "path": "../web-sdk/tsconfig.esm.json" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/web-tracing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.cjs.json" }, 4 | { "path": "./tsconfig.esm.json" }, 5 | { "path": "./tsconfig.spec.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/web-tracing/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.spec.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/types", 5 | "outDir": "./dist/spec", 6 | "rootDir": "..", 7 | "tsBuildInfoFile": "../../.cache/tsc/webTracing.spec.tsbuildinfo" 8 | }, 9 | "include": ["./src"], 10 | "references": [{ "path": "../core/tsconfig.spec.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "ES5", 6 | "verbatimModuleSyntax": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.base.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "target": "ES6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "composite": true, 5 | "declaration": true, 6 | "emitDecoratorMetadata": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "verbatimModuleSyntax": true, 11 | "incremental": true, 12 | "inlineSources": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "lib": ["DOM", "ES6"], 16 | "moduleResolution": "Node", 17 | "noEmitOnError": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitOverride": true, 20 | "noImplicitReturns": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noUncheckedIndexedAccess": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "resolveJsonModule": true, 26 | "rootDir": ".", 27 | "skipDefaultLibCheck": true, 28 | "skipLibCheck": true, 29 | "sourceMap": true, 30 | "strict": true, 31 | "types": ["jest", "node"] 32 | }, 33 | "ts-node": { 34 | "esm": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.base.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.cjs.json", 3 | "compilerOptions": { 4 | "isolatedModules": false, 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | --------------------------------------------------------------------------------