diff --git a/.gitignore b/.gitignore index 4de58cbd..ecb132ea 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ public/uploads/* private/uploads/* private/keys/* +private/telemetry/* + /.env seeds/pocket-id/*.db @@ -64,4 +66,4 @@ certificates .codex /docs .worktrees -.superpowers \ No newline at end of file +.superpowers diff --git a/package.json b/package.json index 1bc88aff..0e452249 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,11 @@ "@hookform/resolvers": "^5.4.0", "@modelcontextprotocol/sdk": "^1.29.0", "@onboardjs/react": "1.0.0-rc.5", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/semantic-conventions": "^1.42.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d284a15..e184e607 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,16 +16,16 @@ importers: version: 12.33.0 '@better-auth/api-key': specifier: 1.6.11 - version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) '@better-auth/core': specifier: 1.6.11 - version: 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + version: 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/passkey': specifier: 1.6.11 - version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))(nanostores@1.3.0) + version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))(nanostores@1.3.0) '@better-auth/sso': specifier: 1.6.11 - version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6)) + version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6)) '@google-cloud/storage': specifier: ^7.19.0 version: 7.21.0 @@ -38,6 +38,21 @@ importers: '@onboardjs/react': specifier: 1.0.0-rc.5 version: 1.0.0-rc.5(@onboardjs/core@1.0.0-rc.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': + specifier: ^2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': + specifier: ^1.42.0 + version: 1.42.0 '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -115,7 +130,7 @@ importers: version: 6.4.24 better-auth: specifier: 1.6.11 - version: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) canvas-confetti: specifier: ^1.9.4 version: 1.9.4 @@ -136,10 +151,10 @@ importers: version: 16.6.1 drizzle-orm: specifier: 0.45.2 - version: 0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) + version: 0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) drizzle-zod: specifier: 0.8.3 - version: 0.8.3(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(zod@4.3.6) + version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(zod@4.3.6) googleapis: specifier: ^170.1.0 version: 170.1.0 @@ -157,10 +172,10 @@ importers: version: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 16.2.3 - version: 16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-safe-action: specifier: ^7.10.8 - version: 7.10.8(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.3.6) + version: 7.10.8(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.3.6) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -227,7 +242,7 @@ importers: version: 6.0.2(react@19.2.7) '@react-email/preview-server': specifier: 4.3.2 - version: 4.3.2(postcss@8.5.10) + version: 4.3.2(@opentelemetry/api@1.9.1)(postcss@8.5.10) '@react-email/render': specifier: ^2.0.8 version: 2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1990,10 +2005,70 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-metrics-otlp-http@0.220.0': + resolution: {integrity: sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.220.0': + resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.220.0': + resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.220.0': + resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.9.0': + resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.41.1': resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.42.0': + resolution: {integrity: sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==} + engines: {node: '>=14'} + '@peculiar/asn1-android@2.8.0': resolution: {integrity: sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==} @@ -7966,75 +8041,77 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@better-auth/api-key@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + '@better-auth/api-key@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 - better-auth: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + better-auth: 1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) zod: 4.3.6 - '@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)': + '@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)': dependencies: '@better-auth/utils': 0.4.0 '@better-fetch/fetch': 1.1.21 - '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/semantic-conventions': 1.42.0 '@standard-schema/spec': 1.1.0 better-call: 1.3.5(zod@4.3.6) jose: 6.2.3 kysely: 0.28.17 nanostores: 1.3.0 zod: 4.3.6 + optionalDependencies: + '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))': + '@better-auth/drizzle-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 optionalDependencies: - drizzle-orm: 0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) - '@better-auth/kysely-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17)': + '@better-auth/kysely-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17)': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 optionalDependencies: kysely: 0.28.17 - '@better-auth/memory-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': + '@better-auth/memory-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 - '@better-auth/mongo-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': + '@better-auth/mongo-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 - '@better-auth/passkey@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))(nanostores@1.3.0)': + '@better-auth/passkey@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))(nanostores@1.3.0)': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 '@better-fetch/fetch': 1.1.21 '@simplewebauthn/browser': 13.3.0 '@simplewebauthn/server': 13.3.2 - better-auth: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + better-auth: 1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) better-call: 1.3.5(zod@4.3.6) nanostores: 1.3.0 zod: 4.3.6 - '@better-auth/prisma-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.7.0(typescript@5.9.3))': + '@better-auth/prisma-adapter@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.7.0(typescript@5.9.3))': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 optionalDependencies: '@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3) prisma: 6.7.0(typescript@5.9.3) - '@better-auth/sso@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))': + '@better-auth/sso@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 '@better-fetch/fetch': 1.1.21 - better-auth: 1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + better-auth: 1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) better-call: 1.3.5(zod@4.3.6) fast-xml-parser: 5.9.3 jose: 6.2.3 @@ -8042,9 +8119,9 @@ snapshots: tldts: 6.1.86 zod: 4.3.6 - '@better-auth/telemetry@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)': + '@better-auth/telemetry@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)': dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.0 '@better-fetch/fetch': 1.1.21 @@ -9065,8 +9142,73 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/exporter-metrics-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/semantic-conventions@1.41.1': {} + '@opentelemetry/semantic-conventions@1.42.0': {} + '@peculiar/asn1-android@2.8.0': dependencies: '@peculiar/asn1-schema': 2.8.0 @@ -10191,7 +10333,7 @@ snapshots: md-to-react-email: 5.0.6(react@19.2.7) react: 19.2.7 - '@react-email/preview-server@4.3.2(postcss@8.5.10)': + '@react-email/preview-server@4.3.2(@opentelemetry/api@1.9.1)(postcss@8.5.10)': dependencies: '@babel/core': 7.26.10 '@babel/parser': 7.27.0 @@ -10217,7 +10359,7 @@ snapshots: json5: 2.2.3 log-symbols: 4.1.0 module-punycode: punycode@2.3.1 - next: 15.5.2(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) node-html-parser: 7.0.1 ora: 5.4.1 pretty-bytes: 6.1.1 @@ -11087,15 +11229,15 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + better-auth@1.6.11(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))) - '@better-auth/kysely-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17) - '@better-auth/memory-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) - '@better-auth/mongo-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) - '@better-auth/prisma-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.7.0(typescript@5.9.3)) - '@better-auth/telemetry': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21) + '@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3))) + '@better-auth/kysely-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(kysely@0.28.17) + '@better-auth/memory-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) + '@better-auth/mongo-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0) + '@better-auth/prisma-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.7.0(typescript@5.9.3)) + '@better-auth/telemetry': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21) '@better-auth/utils': 0.4.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.2.0 @@ -11109,8 +11251,8 @@ snapshots: optionalDependencies: '@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3) drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) - next: 16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) + next: 16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) pg: 8.22.0 prisma: 6.7.0(typescript@5.9.3) react: 19.2.7 @@ -11641,17 +11783,18 @@ snapshots: esbuild: 0.25.12 tsx: 4.22.4 - drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)): + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)): optionalDependencies: + '@opentelemetry/api': 1.9.1 '@prisma/client': 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3) '@types/pg': 8.20.0 kysely: 0.28.17 pg: 8.22.0 prisma: 6.7.0(typescript@5.9.3) - drizzle-zod@0.8.3(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(zod@4.3.6): + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(prisma@6.7.0(typescript@5.9.3)) zod: 4.3.6 dunder-proto@1.0.1: @@ -13362,9 +13505,9 @@ snapshots: dependencies: type-fest: 2.19.0 - next-safe-action@7.10.8(next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.3.6): + next-safe-action@7.10.8(next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.3.6): dependencies: - next: 16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: @@ -13375,7 +13518,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@15.5.2(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.5.2(@babel/core@7.26.10)(@opentelemetry/api@1.9.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.5.2 '@swc/helpers': 0.5.15 @@ -13393,12 +13536,13 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.2 '@next/swc-win32-arm64-msvc': 15.5.2 '@next/swc-win32-x64-msvc': 15.5.2 + '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@16.2.3(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.2.3 '@swc/helpers': 0.5.15 @@ -13417,6 +13561,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.3 '@next/swc-win32-arm64-msvc': 16.2.3 '@next/swc-win32-x64-msvc': 16.2.3 + '@opentelemetry/api': 1.9.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' diff --git a/src/env.mjs b/src/env.mjs index dfda9f66..3ae8f6fa 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -122,6 +122,11 @@ export const env = createEnv({ .enum(["true", "false"]) .default("false") .transform((val) => val === "true"), + + TELEMETRY: z + .enum(["true", "false"]) + .default("true") + .transform((val) => val === "true"), }, client: { NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), @@ -204,6 +209,8 @@ export const env = createEnv({ DEMO_ENABLED: process.env.DEMO_ENABLED, + TELEMETRY: process.env.TELEMETRY, + }, }); diff --git a/src/features/telemetry/actions/telemetry.action.ts b/src/features/telemetry/actions/telemetry.action.ts new file mode 100644 index 00000000..23be1dd7 --- /dev/null +++ b/src/features/telemetry/actions/telemetry.action.ts @@ -0,0 +1,22 @@ +"use server"; + +import { z } from "zod"; +import { action } from "@/lib/safe-actions/actions"; +import { env } from "@/env.mjs"; +import { collectRawTelemetry } from "@/features/telemetry/queries/telemetry.queries"; +import { getOrCreateInstance } from "@/features/telemetry/services/instance"; +import { buildTelemetryPayload } from "@/features/telemetry/services/anonymize"; + +export const compileTelemetryAction = action + .schema(z.object({})) + .action(async () => { + const [raw, instance] = await Promise.all([ + collectRawTelemetry(), + getOrCreateInstance(), + ]); + return buildTelemetryPayload(raw, { + secret: env.PROJECT_SECRET, + version: env.NEXT_PUBLIC_PROJECT_VERSION ?? "unknown", + instanceId: instance.id, + }); + }); diff --git a/src/features/telemetry/constants.ts b/src/features/telemetry/constants.ts new file mode 100644 index 00000000..6f754d88 --- /dev/null +++ b/src/features/telemetry/constants.ts @@ -0,0 +1,11 @@ +export const SERVICE_NAME = "portabase-dashboard"; +export const TELEMETRY_METER_NAME = "portabase-telemetry"; +export const OTLP_AUTH_TOKEN = + "4625713e72b4ec4eaac8a93716f3a83a3afc7fe17dfb8424c57aa82c459d4a8e"; + +export function getOtlpEndpoint(): string { + return process.env.NODE_ENV === "production" + ? "https://telemetry.portabase.io" + : "http://localhost:4318"; + // : "https://sandbox.telemetry.portabase.io"; +} diff --git a/src/features/telemetry/cron.ts b/src/features/telemetry/cron.ts new file mode 100644 index 00000000..38d58181 --- /dev/null +++ b/src/features/telemetry/cron.ts @@ -0,0 +1,37 @@ +import cron from "node-cron"; +import { logger } from "@/lib/logger"; +import { getOrCreateInstance } from "@/features/telemetry/services/instance"; +import { runTelemetry } from "@/features/telemetry/run"; +import { env } from "@/env.mjs"; + +const log = logger.child({ module: "telemetry/cron" }); + +export async function startTelemetryCron(): Promise { + const { schedule, created } = await getOrCreateInstance(); + + if (created) { + console.warn( + "\n" + + "══════════════════════════════════════════════════════════════════════\n" + + "\n" + + " Portabase sends anonymous, aggregated usage metrics to help us\n" + + " understand how it is used in production. Only counts and enum labels\n" + + " are sent. Never personal data, database contents, names, hosts,\n" + + " credentials or configuration. The instance id is a random UUID,\n" + + " hashed before it ever leaves your server.\n" + + "\n" + + ` Details & what we collect: https://portabase.io/privacy\n` + + "\n" + + "══════════════════════════════════════════════════════════════════════" + + "\n", + ); + } + + cron.schedule(env.NODE_ENV === "production" ? schedule : "* * * * *", async () => { + try { + await runTelemetry(); + } catch (err) { + log.error({ job: "cron", name: "telemetryJob", error: err }, "Telemetry Error"); + } + }); +} diff --git a/src/features/telemetry/index.ts b/src/features/telemetry/index.ts new file mode 100644 index 00000000..9d06b856 --- /dev/null +++ b/src/features/telemetry/index.ts @@ -0,0 +1,5 @@ +export { runTelemetry } from "@/features/telemetry/run"; +export { startTelemetryCron } from "@/features/telemetry/cron"; +export { compileTelemetryAction } from "@/features/telemetry/actions/telemetry.action"; +export { getOtlpEndpoint } from "@/features/telemetry/constants"; +export type { TelemetryPayload } from "@/features/telemetry/schemas/telemetry.schema"; diff --git a/src/features/telemetry/otel/export.ts b/src/features/telemetry/otel/export.ts new file mode 100644 index 00000000..30e12218 --- /dev/null +++ b/src/features/telemetry/otel/export.ts @@ -0,0 +1,43 @@ +import type { Meter } from "@opentelemetry/api"; +import { getMeterProvider } from "@/features/telemetry/otel/instrumentation"; +import { TELEMETRY_METER_NAME } from "@/features/telemetry/constants"; +import type { + DistributionEntry, + TelemetryPayload, +} from "@/features/telemetry/schemas/telemetry.schema"; + +function recordDistribution( + meter: Meter, + name: string, + attrKey: string, + entries: DistributionEntry[], +): void { + const gauge = meter.createGauge(name); + for (const entry of entries) { + gauge.record(entry.count, { [attrKey]: entry.label }); + } +} + +export async function exportTelemetry(payload: TelemetryPayload): Promise { + const provider = getMeterProvider(payload.instanceId); + const meter = provider.getMeter(TELEMETRY_METER_NAME); + + meter.createGauge("portabase.users.total").record(payload.usersTotal); + meter.createGauge("portabase.organizations.total").record(payload.orgsTotal); + meter.createGauge("portabase.agents.total").record(payload.agentsTotal); + meter.createGauge("portabase.databases.total").record(payload.databasesTotal); + + meter + .createGauge("portabase.instance.info") + .record(1, { dashboard_version: payload.dashboardVersion }); + + meter.createGauge("portabase.encryption.enabled").record(payload.encryptionEnabled ? 1 : 0); + + recordDistribution(meter, "portabase.databases.by_type", "db_type", payload.databasesByType); + recordDistribution(meter, "portabase.storage.backends", "backend", payload.storageByBackend); + recordDistribution(meter, "portabase.notification.channels", "channel", payload.notificationsByChannel); + recordDistribution(meter, "portabase.agents.by_version", "agent_version", payload.agentsByVersion); + + await provider.forceFlush(); + await provider.shutdown(); +} diff --git a/src/features/telemetry/otel/instrumentation.ts b/src/features/telemetry/otel/instrumentation.ts new file mode 100644 index 00000000..f6892d47 --- /dev/null +++ b/src/features/telemetry/otel/instrumentation.ts @@ -0,0 +1,25 @@ +import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; +import { env } from "@/env.mjs"; +import { getOtlpEndpoint, OTLP_AUTH_TOKEN, SERVICE_NAME } from "@/features/telemetry/constants"; + +export function getMeterProvider(instanceId: string): MeterProvider { + const exporter = new OTLPMetricExporter({ + url: `${getOtlpEndpoint()}/v1/metrics`, + headers: { Authorization: `Bearer ${OTLP_AUTH_TOKEN}` }, + }); + const reader = new PeriodicExportingMetricReader({ + exporter, + exportIntervalMillis: 24 * 60 * 60 * 1000, + }); + return new MeterProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: SERVICE_NAME, + [ATTR_SERVICE_VERSION]: env.NEXT_PUBLIC_PROJECT_VERSION ?? "unknown", + "service.instance.id": instanceId, + }), + readers: [reader], + }); +} diff --git a/src/features/telemetry/queries/telemetry.queries.ts b/src/features/telemetry/queries/telemetry.queries.ts new file mode 100644 index 00000000..e7704d8d --- /dev/null +++ b/src/features/telemetry/queries/telemetry.queries.ts @@ -0,0 +1,64 @@ +import { count } from "drizzle-orm"; +import { db, schemas } from "@/db"; + +export type RawCount = { key: string | null; count: number }; + +export type RawTelemetry = { + orgsTotal: number; + usersTotal: number; + agentsTotal: number; + databasesTotal: number; + databasesByType: RawCount[]; + storageByBackend: RawCount[]; + notificationsByChannel: RawCount[]; + agentsByVersion: RawCount[]; + encryptionEnabled: boolean; +}; + +export async function collectRawTelemetry(): Promise { + const [ + orgs, + users, + agents, + databases, + databasesByType, + storageByBackend, + notificationsByChannel, + agentsByVersion, + settingRow, + ] = await Promise.all([ + db.select({ c: count() }).from(schemas.organization), + db.select({ c: count() }).from(schemas.user), + db.select({ c: count() }).from(schemas.agent), + db.select({ c: count() }).from(schemas.database), + db + .select({ key: schemas.database.dbms, count: count() }) + .from(schemas.database) + .groupBy(schemas.database.dbms), + db + .select({ key: schemas.storageChannel.provider, count: count() }) + .from(schemas.storageChannel) + .groupBy(schemas.storageChannel.provider), + db + .select({ key: schemas.notificationChannel.provider, count: count() }) + .from(schemas.notificationChannel) + .groupBy(schemas.notificationChannel.provider), + db + .select({ key: schemas.agent.version, count: count() }) + .from(schemas.agent) + .groupBy(schemas.agent.version), + db.select({ encryption: schemas.setting.encryption }).from(schemas.setting).limit(1), + ]); + + return { + orgsTotal: orgs[0]?.c ?? 0, + usersTotal: users[0]?.c ?? 0, + agentsTotal: agents[0]?.c ?? 0, + databasesTotal: databases[0]?.c ?? 0, + databasesByType, + storageByBackend, + notificationsByChannel, + agentsByVersion, + encryptionEnabled: settingRow[0]?.encryption ?? false, + }; +} diff --git a/src/features/telemetry/run.ts b/src/features/telemetry/run.ts new file mode 100644 index 00000000..002f064b --- /dev/null +++ b/src/features/telemetry/run.ts @@ -0,0 +1,21 @@ +import { logger } from "@/lib/logger"; +import { compileTelemetryAction } from "@/features/telemetry/actions/telemetry.action"; +import { exportTelemetry } from "@/features/telemetry/otel/export"; + +const log = logger.child({ module: "telemetry" }); + +export async function runTelemetry(): Promise { + const result = await compileTelemetryAction({}); + const payload = result?.data; + + if (!payload) { + log.error( + { serverError: result?.serverError, validationErrors: result?.validationErrors }, + "Telemetry compile failed", + ); + return; + } + + await exportTelemetry(payload); + log.info({ instanceId: payload.instanceId }, `Telemetry exported (${payload.instanceId})`); +} diff --git a/src/features/telemetry/schemas/telemetry.schema.ts b/src/features/telemetry/schemas/telemetry.schema.ts new file mode 100644 index 00000000..3425f956 --- /dev/null +++ b/src/features/telemetry/schemas/telemetry.schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const distributionEntrySchema = z.object({ + label: z.string(), + count: z.number().int().nonnegative(), +}); + +export const telemetryPayloadSchema = z.object({ + instanceId: z.string(), + dashboardVersion: z.string(), + orgsTotal: z.number().int().nonnegative(), + usersTotal: z.number().int().nonnegative(), + agentsTotal: z.number().int().nonnegative(), + databasesTotal: z.number().int().nonnegative(), + databasesByType: z.array(distributionEntrySchema), + storageByBackend: z.array(distributionEntrySchema), + notificationsByChannel: z.array(distributionEntrySchema), + agentsByVersion: z.array(distributionEntrySchema), + encryptionEnabled: z.boolean(), +}); + +export type DistributionEntry = z.infer; +export type TelemetryPayload = z.infer; diff --git a/src/features/telemetry/services/anonymize.ts b/src/features/telemetry/services/anonymize.ts new file mode 100644 index 00000000..76053cc6 --- /dev/null +++ b/src/features/telemetry/services/anonymize.ts @@ -0,0 +1,40 @@ +import { createHash } from "crypto"; +import type { RawTelemetry, RawCount } from "@/features/telemetry/queries/telemetry.queries"; +import { + telemetryPayloadSchema, + type DistributionEntry, + type TelemetryPayload, +} from "@/features/telemetry/schemas/telemetry.schema"; + +export type AnonymizeContext = { secret: string; version: string; instanceId: string }; + +export function hashInstanceId(instanceId: string, secret: string): string { + return createHash("sha256").update(`${instanceId}${secret}`).digest("hex"); +} + +function mapDistribution(rows: RawCount[]): DistributionEntry[] { + return rows.map((r) => ({ + label: r.key ?? "unknown", + count: r.count, + })); +} + +export function buildTelemetryPayload( + raw: RawTelemetry, + ctx: AnonymizeContext, +): TelemetryPayload { + const payload: TelemetryPayload = { + instanceId: hashInstanceId(ctx.instanceId, ctx.secret), + dashboardVersion: ctx.version, + orgsTotal: raw.orgsTotal, + usersTotal: raw.usersTotal, + agentsTotal: raw.agentsTotal, + databasesTotal: raw.databasesTotal, + databasesByType: mapDistribution(raw.databasesByType), + storageByBackend: mapDistribution(raw.storageByBackend), + notificationsByChannel: mapDistribution(raw.notificationsByChannel), + agentsByVersion: mapDistribution(raw.agentsByVersion), + encryptionEnabled: raw.encryptionEnabled, + }; + return telemetryPayloadSchema.parse(payload); +} diff --git a/src/features/telemetry/services/instance.ts b/src/features/telemetry/services/instance.ts new file mode 100644 index 00000000..9efe4ba9 --- /dev/null +++ b/src/features/telemetry/services/instance.ts @@ -0,0 +1,35 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { randomUUID } from "crypto"; +import { env } from "@/env.mjs"; + +export type InstanceData = { id: string; schedule: string; created: boolean }; + +function randomDailySchedule(): string { + const minute = Math.floor(Math.random() * 60); + const hour = Math.floor(Math.random() * 24); + return `${minute} ${hour} * * *`; +} + +export async function getOrCreateInstance( + filePath = path.join(env.PRIVATE_PATH!, "telemetry", "data.json"), +): Promise { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + + let data: Partial = {}; + try { + const raw = await fs.readFile(filePath, "utf8"); + data = JSON.parse(raw) as Partial; + } catch { + } + + const created = !data.id; + const id = data.id ?? randomUUID(); + const schedule = data.schedule ?? randomDailySchedule(); + + if (data.id !== id || data.schedule !== schedule) { + await fs.writeFile(filePath, JSON.stringify({ id, schedule }, null, 2), { mode: 0o600 }); + } + + return { id, schedule, created }; +} diff --git a/src/utils/init/cron.ts b/src/utils/init/cron.ts index 7db2077f..e3f017bf 100644 --- a/src/utils/init/cron.ts +++ b/src/utils/init/cron.ts @@ -1,5 +1,7 @@ import {cleaningHealthcheckLogsJob, cleaningJob, healthcheckAgentAndDatabaseJob, retentionJob} from "@/lib/tasks"; import {logger} from "@/lib/logger"; +import { env } from "@/env.mjs"; +import { startTelemetryCron } from "@/features/telemetry/cron"; const log = logger.child({module: "init/cron"}); @@ -10,5 +12,8 @@ export async function setupCronJobs() { cleaningJob.start(); cleaningHealthcheckLogsJob.start(); healthcheckAgentAndDatabaseJob.start(); + if (env.TELEMETRY) { + await startTelemetryCron(); + } log.info("==== Cron jobs started ===="); -} \ No newline at end of file +}