diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 6e57ee2ea812..beb758aca018 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -74,6 +74,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/effect': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/ember': access: $all publish: $all diff --git a/package.json b/package.json index 91bc549e4527..59d0415142c1 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "packages/core", "packages/cloudflare", "packages/deno", + "packages/effect", "packages/ember", "packages/eslint-config-sdk", "packages/eslint-plugin-sdk", diff --git a/packages/effect/.eslintrc.js b/packages/effect/.eslintrc.js new file mode 100644 index 000000000000..d37e458c151c --- /dev/null +++ b/packages/effect/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + node: true, + }, + overrides: [ + { + files: ['vite.config.ts', 'vitest.config.ts'], + parserOptions: { + project: ['tsconfig.vite.json'], + }, + }, + ], + extends: ['../../.eslintrc.js'], +}; diff --git a/packages/effect/LICENSE b/packages/effect/LICENSE new file mode 100644 index 000000000000..fea6013e7dbf --- /dev/null +++ b/packages/effect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Functional Software Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/effect/README.md b/packages/effect/README.md new file mode 100644 index 000000000000..a209c930b659 --- /dev/null +++ b/packages/effect/README.md @@ -0,0 +1,47 @@ +# Official Sentry SDK for Effect.ts (Alpha) + +[![npm version](https://img.shields.io/npm/v/@sentry/effect.svg)](https://www.npmjs.com/package/@sentry/effect) +[![npm dm](https://img.shields.io/npm/dm/@sentry/effect.svg)](https://www.npmjs.com/package/@sentry/effect) +[![npm dt](https://img.shields.io/npm/dt/@sentry/effect.svg)](https://www.npmjs.com/package/@sentry/effect) + +> NOTICE: This package is in alpha state and may be subject to breaking changes. + +## Getting Started + +This SDK does not have docs yet. Stay tuned. + +## Usage + +```typescript +import * as Sentry from '@sentry/effect/server'; +import { NodeRuntime } from '@effect/platform-node'; +import { Layer } from 'effect'; +import { HttpLive } from './Http.js'; + +const MainLive = HttpLive.pipe( + Layer.provide( + Sentry.effectLayer({ + dsn: '__DSN__', + enableLogs: true, + enableMetrics: true, + }), + ), +); + +MainLive.pipe(Layer.launch, NodeRuntime.runMain); +``` + +The `effectLayer` function initializes Sentry and returns an Effect Layer that provides: + +- Distributed tracing with automatic HTTP header extraction/injection +- Effect spans traced as Sentry spans +- Effect logs forwarded to Sentry (when `enableLogs` is set) +- Effect metrics sent to Sentry (when `enableMetrics` is set) + +## Links + + + +- [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_effect) +- [Sentry Discord Server](https://discord.gg/Ww9hbqr) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry) diff --git a/packages/effect/package.json b/packages/effect/package.json new file mode 100644 index 000000000000..c088def04907 --- /dev/null +++ b/packages/effect/package.json @@ -0,0 +1,99 @@ +{ + "name": "@sentry/effect", + "version": "10.42.0", + "description": "Official Sentry SDK for Effect", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/effect", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "files": [ + "/build" + ], + "main": "build/cjs/index.server.js", + "module": "build/esm/index.server.js", + "browser": "build/esm/index.client.js", + "types": "build/types/index.types.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./build/types/index.types.d.ts", + "browser": { + "import": "./build/esm/index.client.js", + "require": "./build/cjs/index.client.js" + }, + "node": { + "import": "./build/esm/index.server.js", + "require": "./build/cjs/index.server.js" + } + }, + "./server": { + "types": "./build/types/index.server.d.ts", + "import": "./build/esm/index.server.js", + "require": "./build/cjs/index.server.js" + }, + "./client": { + "types": "./build/types/index.client.d.ts", + "import": "./build/esm/index.client.js", + "require": "./build/cjs/index.client.js" + } + }, + "typesVersions": { + "<5.0": { + "build/types/index.types.d.ts": [ + "build/types-ts3.8/index.types.d.ts" + ], + "build/types/index.server.d.ts": [ + "build/types-ts3.8/index.server.d.ts" + ], + "build/types/index.client.d.ts": [ + "build/types-ts3.8/index.client.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/browser": "10.42.0", + "@sentry/core": "10.42.0", + "@sentry/node-core": "10.42.0" + }, + "peerDependencies": { + "effect": "^3.0.0" + }, + "peerDependenciesMeta": { + "effect": { + "optional": false + } + }, + "devDependencies": { + "@effect/vitest": "^0.23.9", + "effect": "^3.19.19" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:tarball": "npm pack", + "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", + "clean": "rimraf build coverage sentry-effect-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "test": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/effect/rollup.npm.config.mjs b/packages/effect/rollup.npm.config.mjs new file mode 100644 index 000000000000..211157646473 --- /dev/null +++ b/packages/effect/rollup.npm.config.mjs @@ -0,0 +1,25 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +const baseConfig = makeBaseNPMConfig({ + entrypoints: ['src/index.server.ts', 'src/index.client.ts'], + packageSpecificConfig: { + output: { + preserveModulesRoot: 'src', + }, + }, +}); + +const defaultExternal = baseConfig.external || []; +baseConfig.external = id => { + if (defaultExternal.includes(id)) { + return true; + } + + if (id === 'effect' || id.startsWith('effect/') || id.startsWith('@sentry/')) { + return true; + } + + return false; +}; + +export default makeNPMConfigVariants(baseConfig); diff --git a/packages/effect/src/client/index.ts b/packages/effect/src/client/index.ts new file mode 100644 index 000000000000..34dfae6cb7c8 --- /dev/null +++ b/packages/effect/src/client/index.ts @@ -0,0 +1,39 @@ +import type { BrowserOptions } from '@sentry/browser'; +import type * as EffectLayer from 'effect/Layer'; +import { suspend as suspendLayer } from 'effect/Layer'; +import { buildEffectLayer } from '../utils/buildEffectLayer'; +import { init } from './sdk'; + +export { init } from './sdk'; + +/** + * Options for the Sentry Effect client layer. + */ +export type EffectClientLayerOptions = BrowserOptions; + +/** + * Creates an Effect Layer that initializes Sentry for browser clients. + * + * This layer provides Effect applications with full Sentry instrumentation including: + * - Effect spans traced as Sentry spans + * - Effect logs forwarded to Sentry (when `enableLogs` is set) + * + * @example + * ```typescript + * import * as Sentry from '@sentry/effect/client'; + * import { Layer, Effect } from 'effect'; + * + * const ApiClientWithSentry = ApiClientLive.pipe( + * Layer.provide(Sentry.effectLayer({ + * dsn: '__DSN__', + * integrations: [Sentry.browserTracingIntegration()], + * tracesSampleRate: 1.0, + * })), + * ); + * + * Effect.runPromise(Effect.provide(myEffect, ApiClientWithSentry)); + * ``` + */ +export function effectLayer(options: EffectClientLayerOptions): EffectLayer.Layer { + return suspendLayer(() => buildEffectLayer(options, init(options))); +} diff --git a/packages/effect/src/client/sdk.ts b/packages/effect/src/client/sdk.ts new file mode 100644 index 000000000000..5f2210a92b3a --- /dev/null +++ b/packages/effect/src/client/sdk.ts @@ -0,0 +1,20 @@ +import type { BrowserOptions } from '@sentry/browser'; +import { init as initBrowser } from '@sentry/browser'; +import type { Client } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; + +/** + * Initializes the Sentry Effect SDK for browser clients. + * + * @param options - Configuration options for the SDK + * @returns The initialized Sentry client, or undefined if initialization failed + */ +export function init(options: BrowserOptions): Client | undefined { + const opts = { + ...options, + }; + + applySdkMetadata(opts, 'effect', ['effect', 'browser']); + + return initBrowser(opts); +} diff --git a/packages/effect/src/index.client.ts b/packages/effect/src/index.client.ts new file mode 100644 index 000000000000..e13f1ddea09e --- /dev/null +++ b/packages/effect/src/index.client.ts @@ -0,0 +1,7 @@ +// import/export got a false positive, and affects most of our index barrel files +// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 +/* eslint-disable import/export */ +export * from '@sentry/browser'; + +export { effectLayer, init } from './client/index'; +export type { EffectClientLayerOptions } from './client/index'; diff --git a/packages/effect/src/index.server.ts b/packages/effect/src/index.server.ts new file mode 100644 index 000000000000..a3f8e4f3766f --- /dev/null +++ b/packages/effect/src/index.server.ts @@ -0,0 +1,4 @@ +export * from '@sentry/node-core/light'; + +export { effectLayer, init } from './server/index'; +export type { EffectServerLayerOptions } from './server/index'; diff --git a/packages/effect/src/index.types.ts b/packages/effect/src/index.types.ts new file mode 100644 index 000000000000..e0a6e9512eeb --- /dev/null +++ b/packages/effect/src/index.types.ts @@ -0,0 +1,26 @@ +/* eslint-disable import/export */ + +// We export everything from both the client part of the SDK and from the server part. +// Some of the exports collide, which is not allowed, unless we redefine the colliding +// exports in this file - which we do below. +import type { Client, Integration, Options, StackParser } from '@sentry/core'; +import type * as EffectLayer from 'effect/Layer'; +import type * as clientSdk from './index.client'; +import type * as serverSdk from './index.server'; + +export * from './index.client'; +export * from './index.server'; + +export type { EffectClientLayerOptions } from './index.client'; +export type { EffectServerLayerOptions } from './index.server'; + +export declare function effectLayer( + options: clientSdk.EffectClientLayerOptions | serverSdk.EffectServerLayerOptions, +): EffectLayer.Layer; + +export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): Client | undefined; +export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; +export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const getDefaultIntegrations: (options: Options) => Integration[]; +export declare const defaultStackParser: StackParser; +export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; diff --git a/packages/effect/src/logger.ts b/packages/effect/src/logger.ts new file mode 100644 index 000000000000..833f5b6b7e95 --- /dev/null +++ b/packages/effect/src/logger.ts @@ -0,0 +1,43 @@ +import { logger as sentryLogger } from '@sentry/core'; +import * as Logger from 'effect/Logger'; + +/** + * Effect Logger that sends logs to Sentry. + */ +export const SentryEffectLogger = Logger.make(({ logLevel, message }) => { + let msg: string; + if (typeof message === 'string') { + msg = message; + } else if (Array.isArray(message) && message.length === 1) { + const firstElement = message[0]; + msg = typeof firstElement === 'string' ? firstElement : JSON.stringify(firstElement); + } else { + msg = JSON.stringify(message); + } + + switch (logLevel._tag) { + case 'Fatal': + sentryLogger.fatal(msg); + break; + case 'Error': + sentryLogger.error(msg); + break; + case 'Warning': + sentryLogger.warn(msg); + break; + case 'Info': + sentryLogger.info(msg); + break; + case 'Debug': + sentryLogger.debug(msg); + break; + case 'Trace': + sentryLogger.trace(msg); + break; + case 'All': + case 'None': + break; + default: + logLevel satisfies never; + } +}); diff --git a/packages/effect/src/server/index.ts b/packages/effect/src/server/index.ts new file mode 100644 index 000000000000..2dcca1f7a4e2 --- /dev/null +++ b/packages/effect/src/server/index.ts @@ -0,0 +1,40 @@ +import type { NodeOptions } from '@sentry/node-core/light'; +import type * as EffectLayer from 'effect/Layer'; +import { buildEffectLayer } from '../utils/buildEffectLayer'; +import { init } from './sdk'; + +export { init } from './sdk'; + +/** + * Options for the Sentry Effect server layer. + */ +export type EffectServerLayerOptions = NodeOptions; + +/** + * Creates an Effect Layer that initializes Sentry for Node.js servers. + * + * This layer provides Effect applications with full Sentry instrumentation including: + * - Effect spans traced as Sentry spans + * - Effect logs forwarded to Sentry (when `enableLogs` is set) + * + * @example + * ```typescript + * import * as Sentry from '@sentry/effect/server'; + * import { NodeRuntime } from '@effect/platform-node'; + * import { Layer } from 'effect'; + * import { HttpLive } from './Http.js'; + * + * const MainLive = HttpLive.pipe( + * Layer.provide(Sentry.effectLayer({ + * dsn: '__DSN__', + * enableLogs: true, + * enableMetrics: true, + * })), + * ); + * + * MainLive.pipe(Layer.launch, NodeRuntime.runMain); + * ``` + */ +export function effectLayer(options: EffectServerLayerOptions): EffectLayer.Layer { + return buildEffectLayer(options, init(options)); +} diff --git a/packages/effect/src/server/sdk.ts b/packages/effect/src/server/sdk.ts new file mode 100644 index 000000000000..ee910be13487 --- /dev/null +++ b/packages/effect/src/server/sdk.ts @@ -0,0 +1,20 @@ +import type { Client } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; +import type { NodeOptions } from '@sentry/node-core/light'; +import { init as initNode } from '@sentry/node-core/light'; + +/** + * Initializes the Sentry Effect SDK for Node.js servers. + * + * @param options - Configuration options for the SDK + * @returns The initialized Sentry client, or undefined if initialization failed + */ +export function init(options: NodeOptions): Client | undefined { + const opts = { + ...options, + }; + + applySdkMetadata(opts, 'effect', ['effect', 'node-light']); + + return initNode(opts); +} diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts new file mode 100644 index 000000000000..116b7970a6ae --- /dev/null +++ b/packages/effect/src/tracer.ts @@ -0,0 +1,201 @@ +import type { Span } from '@sentry/core'; +import { + getActiveSpan, + getIsolationScope, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + startInactiveSpan, + withActiveSpan, +} from '@sentry/core'; +import type * as Context from 'effect/Context'; +import * as Exit from 'effect/Exit'; +import type * as Layer from 'effect/Layer'; +import { setTracer } from 'effect/Layer'; +import * as Option from 'effect/Option'; +import * as EffectTracer from 'effect/Tracer'; + +const KIND_MAP: Record = { + internal: 'internal', + client: 'client', + server: 'server', + producer: 'producer', + consumer: 'consumer', +}; + +function deriveOp(name: string, kind: EffectTracer.SpanKind): string { + if (name.startsWith('http.server')) { + return 'http.server'; + } + + if (name.startsWith('http.client')) { + return 'http.client'; + } + + return KIND_MAP[kind]; +} + +function deriveOrigin(name: string): string { + if (name.startsWith('http.server') || name.startsWith('http.client')) { + return 'auto.http.effect'; + } + + return 'auto.function.effect'; +} + +function deriveSpanName(name: string, kind: EffectTracer.SpanKind): string { + if (name.startsWith('http.server') && kind === 'server') { + const isolationScope = getIsolationScope(); + const transactionName = isolationScope.getScopeData().transactionName; + if (transactionName) { + return transactionName; + } + } + return name; +} + +type HrTime = [number, number]; + +const SENTRY_SPAN_SYMBOL = Symbol.for('@sentry/effect.SentrySpan'); + +function nanosToHrTime(nanos: bigint): HrTime { + const seconds = Number(nanos / BigInt(1_000_000_000)); + const remainingNanos = Number(nanos % BigInt(1_000_000_000)); + return [seconds, remainingNanos]; +} + +interface SentrySpanLike extends EffectTracer.Span { + readonly [SENTRY_SPAN_SYMBOL]: true; + readonly sentrySpan: Span; +} + +function isSentrySpan(span: EffectTracer.AnySpan): span is SentrySpanLike { + return SENTRY_SPAN_SYMBOL in span; +} + +class SentrySpanWrapper implements SentrySpanLike { + public readonly [SENTRY_SPAN_SYMBOL]: true; + public readonly _tag: 'Span'; + public readonly spanId: string; + public readonly traceId: string; + public readonly attributes: Map; + public readonly sampled: boolean; + public readonly parent: Option.Option; + public readonly links: Array; + public status: EffectTracer.SpanStatus; + public readonly sentrySpan: Span; + + public constructor( + public readonly name: string, + parent: Option.Option, + public readonly context: Context.Context, + links: ReadonlyArray, + startTime: bigint, + public readonly kind: EffectTracer.SpanKind, + existingSpan: Span, + ) { + this[SENTRY_SPAN_SYMBOL] = true as const; + this._tag = 'Span' as const; + this.attributes = new Map(); + this.parent = parent; + this.links = [...links]; + this.sentrySpan = existingSpan; + + const spanContext = this.sentrySpan.spanContext(); + this.spanId = spanContext.spanId; + this.traceId = spanContext.traceId; + this.sampled = this.sentrySpan.isRecording(); + this.status = { + _tag: 'Started', + startTime, + }; + } + + public attribute(key: string, value: unknown): void { + if (!this.sentrySpan.isRecording()) { + return; + } + + this.sentrySpan.setAttribute(key, value as Parameters[1]); + this.attributes.set(key, value); + } + + public addLinks(links: ReadonlyArray): void { + this.links.push(...links); + } + + public end(endTime: bigint, exit: Exit.Exit): void { + this.status = { + _tag: 'Ended', + endTime, + exit, + startTime: this.status.startTime, + }; + + if (!this.sentrySpan.isRecording()) { + return; + } + + if (Exit.isFailure(exit)) { + const cause = exit.cause; + const message = + cause._tag === 'Fail' ? String(cause.error) : cause._tag === 'Die' ? String(cause.defect) : 'internal_error'; + this.sentrySpan.setStatus({ code: 2, message }); + } else { + this.sentrySpan.setStatus({ code: 1 }); + } + + this.sentrySpan.end(nanosToHrTime(endTime)); + } + + public event(name: string, startTime: bigint, attributes?: Record): void { + if (!this.sentrySpan.isRecording()) { + return; + } + + this.sentrySpan.addEvent(name, attributes as Parameters[1], nanosToHrTime(startTime)); + } +} + +function createSentrySpan( + name: string, + parent: Option.Option, + context: Context.Context, + links: ReadonlyArray, + startTime: bigint, + kind: EffectTracer.SpanKind, +): SentrySpanLike { + const parentSentrySpan = + Option.isSome(parent) && isSentrySpan(parent.value) ? parent.value.sentrySpan : (getActiveSpan() ?? null); + + const spanName = deriveSpanName(name, kind); + + const newSpan = startInactiveSpan({ + name: spanName, + op: deriveOp(name, kind), + startTime: nanosToHrTime(startTime), + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: deriveOrigin(name), + }, + ...(parentSentrySpan ? { parentSpan: parentSentrySpan } : {}), + }); + + return new SentrySpanWrapper(name, parent, context, links, startTime, kind, newSpan); +} + +const makeSentryTracer = (): EffectTracer.Tracer => + EffectTracer.make({ + span(name, parent, context, links, startTime, kind) { + return createSentrySpan(name, parent, context, links, startTime, kind); + }, + context(execution, fiber) { + const currentSpan = fiber.currentSpan; + if (currentSpan === undefined || !isSentrySpan(currentSpan)) { + return execution(); + } + return withActiveSpan(currentSpan.sentrySpan, execution); + }, + }); + +/** + * Effect Layer that sets up the Sentry tracer for Effect spans. + */ +export const SentryEffectTracerLayer: Layer.Layer = setTracer(makeSentryTracer()); diff --git a/packages/effect/src/utils/buildEffectLayer.ts b/packages/effect/src/utils/buildEffectLayer.ts new file mode 100644 index 000000000000..475d2d2a70c3 --- /dev/null +++ b/packages/effect/src/utils/buildEffectLayer.ts @@ -0,0 +1,35 @@ +import type * as EffectLayer from 'effect/Layer'; +import { empty as emptyLayer, provideMerge } from 'effect/Layer'; +import { defaultLogger, replace as replaceLogger } from 'effect/Logger'; +import { SentryEffectLogger } from '../logger'; +import { SentryEffectTracerLayer } from '../tracer'; + +export interface EffectLayerBaseOptions { + enableLogs?: boolean; +} + +/** + * Builds an Effect layer that integrates Sentry tracing and logging. + * + * Returns an empty layer if no Sentry client is available. Otherwise, starts with + * the Sentry tracer layer and optionally merges logging and metrics layers based + * on the provided options. + */ +export function buildEffectLayer( + options: T, + client: unknown, +): EffectLayer.Layer { + if (!client) { + return emptyLayer; + } + + const { enableLogs = false } = options; + let layer: EffectLayer.Layer = SentryEffectTracerLayer; + + if (enableLogs) { + const effectLogger = replaceLogger(defaultLogger, SentryEffectLogger); + layer = layer.pipe(provideMerge(effectLogger)); + } + + return layer; +} diff --git a/packages/effect/test/buildEffectLayer.test.ts b/packages/effect/test/buildEffectLayer.test.ts new file mode 100644 index 000000000000..9875cfe5b14b --- /dev/null +++ b/packages/effect/test/buildEffectLayer.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it, vi } from '@effect/vitest'; +import * as sentryCore from '@sentry/core'; +import { logger as sentryLogger } from '@sentry/core'; +import { Effect, Layer } from 'effect'; +import { empty as emptyLayer } from 'effect/Layer'; +import { buildEffectLayer } from '../src/utils/buildEffectLayer'; + +describe('buildEffectLayer', () => { + describe('when client is falsy', () => { + it('returns empty layer when client is null', () => { + const layer = buildEffectLayer({}, null); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + expect(layer).toBe(emptyLayer); + }); + + it('returns empty layer when client is undefined', () => { + const layer = buildEffectLayer({}, undefined); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + expect(layer).toBe(emptyLayer); + }); + }); + + describe('when client is truthy', () => { + const mockClient = { mock: true }; + + it('returns a valid layer with default options', () => { + const layer = buildEffectLayer({}, mockClient); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + + it('returns a valid layer with enableLogs: false', () => { + const layer = buildEffectLayer({ enableLogs: false }, mockClient); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + + it('returns a valid layer with enableLogs: true', () => { + const layer = buildEffectLayer({ enableLogs: true }, mockClient); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + + it('returns a valid layer with all features enabled', () => { + const layer = buildEffectLayer({ enableLogs: true }, mockClient); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + + it.effect('layer can be provided to an Effect program', () => + Effect.gen(function* () { + const result = yield* Effect.succeed('test-result'); + expect(result).toBe('test-result'); + }).pipe(Effect.provide(buildEffectLayer({}, mockClient))), + ); + + it.effect('layer with logs enabled routes Effect logs to Sentry logger', () => + Effect.gen(function* () { + const infoSpy = vi.spyOn(sentryLogger, 'info'); + yield* Effect.log('test log message'); + expect(infoSpy).toHaveBeenCalledWith('test log message'); + infoSpy.mockRestore(); + }).pipe(Effect.provide(buildEffectLayer({ enableLogs: true }, mockClient))), + ); + + it.effect('layer with logs disabled routes Effect does not log to Sentry logger', () => + Effect.gen(function* () { + const infoSpy = vi.spyOn(sentryLogger, 'info'); + yield* Effect.log('test log message'); + expect(infoSpy).not.toHaveBeenCalled(); + infoSpy.mockRestore(); + }).pipe(Effect.provide(buildEffectLayer({ enableLogs: false }, mockClient))), + ); + + it.effect('layer with all features enabled can be provided to an Effect program', () => + Effect.gen(function* () { + const result = yield* Effect.succeed('all-features'); + expect(result).toBe('all-features'); + }).pipe(Effect.provide(buildEffectLayer({ enableLogs: true }, mockClient))), + ); + + it.effect('layer enables tracing for Effect spans via Sentry tracer', () => + Effect.gen(function* () { + const startInactiveSpanSpy = vi.spyOn(sentryCore, 'startInactiveSpan'); + const result = yield* Effect.withSpan('test-sentry-span')(Effect.succeed('traced')); + expect(result).toBe('traced'); + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'test-sentry-span', + }), + ); + startInactiveSpanSpy.mockRestore(); + }).pipe(Effect.provide(buildEffectLayer({}, mockClient))), + ); + }); + + describe('with additional options', () => { + const mockClient = { mock: true }; + + it('accepts options with additional properties', () => { + const layer = buildEffectLayer( + { + enableLogs: true, + dsn: 'https://test@sentry.io/123', + debug: true, + } as { enableLogs?: boolean; dsn?: string; debug?: boolean }, + mockClient, + ); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + }); +}); diff --git a/packages/effect/test/index.test.ts b/packages/effect/test/index.test.ts new file mode 100644 index 000000000000..950ec06fb670 --- /dev/null +++ b/packages/effect/test/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest'; +import * as index from '../src/index.client'; + +describe('effect index export', () => { + it('has correct exports', () => { + expect(index.captureException).toBeDefined(); + }); +}); diff --git a/packages/effect/test/layer.test.ts b/packages/effect/test/layer.test.ts new file mode 100644 index 000000000000..072d8becb601 --- /dev/null +++ b/packages/effect/test/layer.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from '@effect/vitest'; +import { getClient, getCurrentScope, getIsolationScope, SDK_VERSION } from '@sentry/core'; +import { Effect, Layer } from 'effect'; +import { afterEach, beforeEach, vi } from 'vitest'; +import * as sentryClient from '../src/index.client'; +import * as sentryServer from '../src/index.server'; + +const TEST_DSN = 'https://username@domain/123'; + +function getMockTransport() { + return () => ({ + send: vi.fn().mockResolvedValue({}), + flush: vi.fn().mockResolvedValue(true), + }); +} + +describe.each([ + [{ subSdkName: 'browser', effectLayer: sentryClient.effectLayer }], + [{ subSdkName: 'node-light', effectLayer: sentryServer.effectLayer }], +])('effectLayer ($subSdkName)', ({ subSdkName, effectLayer }) => { + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + }); + + afterEach(() => { + getCurrentScope().setClient(undefined); + }); + + it('creates a valid Effect layer', () => { + const layer = effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }); + + expect(layer).toBeDefined(); + expect(Layer.isLayer(layer)).toBe(true); + }); + + it.effect('applies SDK metadata', () => + Effect.gen(function* () { + yield* Effect.void; + + const client = getClient(); + const metadata = client?.getOptions()._metadata?.sdk; + + expect(metadata?.name).toBe('sentry.javascript.effect'); + expect(metadata?.packages).toEqual([ + { name: 'npm:@sentry/effect', version: SDK_VERSION }, + { name: `npm:@sentry/${subSdkName}`, version: SDK_VERSION }, + ]); + }).pipe( + Effect.provide( + effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }), + ), + ), + ); + + it('creates layer with logs enabled', () => { + const layer = effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + enableLogs: true, + }); + + expect(layer).toBeDefined(); + }); + + it('creates layer with all features enabled', () => { + const layer = effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + enableLogs: true, + }); + + expect(layer).toBeDefined(); + }); + + it.effect('layer can be provided to an Effect program', () => + Effect.gen(function* () { + const result = yield* Effect.succeed('test-result'); + expect(result).toBe('test-result'); + }).pipe( + Effect.provide( + effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }), + ), + ), + ); + + it.effect('layer enables tracing for Effect spans', () => + Effect.gen(function* () { + const result = yield* Effect.withSpan('test-span')(Effect.succeed('traced')); + expect(result).toBe('traced'); + }).pipe( + Effect.provide( + effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }), + ), + ), + ); + + it.effect('layer can be composed with other layers', () => + Effect.gen(function* () { + const result = yield* Effect.succeed(42).pipe( + Effect.map(n => n * 2), + Effect.withSpan('computation'), + ); + expect(result).toBe(84); + }).pipe( + Effect.provide( + effectLayer({ + dsn: TEST_DSN, + transport: getMockTransport(), + }), + ), + ), + ); +}); diff --git a/packages/effect/test/logger.test.ts b/packages/effect/test/logger.test.ts new file mode 100644 index 000000000000..c372784b483f --- /dev/null +++ b/packages/effect/test/logger.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from '@effect/vitest'; +import * as sentryCore from '@sentry/core'; +import { Effect, Layer, Logger, LogLevel } from 'effect'; +import { afterEach, vi } from 'vitest'; +import { SentryEffectLogger } from '../src/logger'; + +vi.mock('@sentry/core', async importOriginal => { + const original = await importOriginal(); + return { + ...original, + logger: { + ...original.logger, + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + fatal: vi.fn(), + }, + }; +}); + +describe('SentryEffectLogger', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + const loggerLayer = Layer.mergeAll( + Logger.replace(Logger.defaultLogger, SentryEffectLogger), + Logger.minimumLogLevel(LogLevel.All), + ); + + it.effect('forwards fatal logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logFatal('This is a fatal message'); + expect(sentryCore.logger.fatal).toHaveBeenCalledWith('This is a fatal message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('forwards error logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logError('This is an error message'); + expect(sentryCore.logger.error).toHaveBeenCalledWith('This is an error message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('forwards warning logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logWarning('This is a warning message'); + expect(sentryCore.logger.warn).toHaveBeenCalledWith('This is a warning message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('forwards info logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logInfo('This is an info message'); + expect(sentryCore.logger.info).toHaveBeenCalledWith('This is an info message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('forwards debug logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logDebug('This is a debug message'); + expect(sentryCore.logger.debug).toHaveBeenCalledWith('This is a debug message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('forwards trace logs to Sentry', () => + Effect.gen(function* () { + yield* Effect.logTrace('This is a trace message'); + expect(sentryCore.logger.trace).toHaveBeenCalledWith('This is a trace message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('handles object messages by stringifying', () => + Effect.gen(function* () { + yield* Effect.logInfo({ key: 'value', nested: { foo: 'bar' } }); + expect(sentryCore.logger.info).toHaveBeenCalledWith('{"key":"value","nested":{"foo":"bar"}}'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('handles multiple log calls', () => + Effect.gen(function* () { + yield* Effect.logInfo('First message'); + yield* Effect.logInfo('Second message'); + yield* Effect.logWarning('Third message'); + expect(sentryCore.logger.info).toHaveBeenCalledTimes(2); + expect(sentryCore.logger.info).toHaveBeenNthCalledWith(1, 'First message'); + expect(sentryCore.logger.info).toHaveBeenNthCalledWith(2, 'Second message'); + expect(sentryCore.logger.warn).toHaveBeenCalledWith('Third message'); + }).pipe(Effect.provide(loggerLayer)), + ); + + it.effect('works with Effect.tap for logging side effects', () => + Effect.gen(function* () { + const result = yield* Effect.succeed('data').pipe( + Effect.tap(data => Effect.logInfo(`Processing: ${data}`)), + Effect.map(d => d.toUpperCase()), + ); + expect(result).toBe('DATA'); + expect(sentryCore.logger.info).toHaveBeenCalledWith('Processing: data'); + }).pipe(Effect.provide(loggerLayer)), + ); +}); diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts new file mode 100644 index 000000000000..8955200695fa --- /dev/null +++ b/packages/effect/test/tracer.test.ts @@ -0,0 +1,306 @@ +import { describe, expect, it } from '@effect/vitest'; +import * as sentryCore from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { Effect } from 'effect'; +import { afterEach, vi } from 'vitest'; +import { SentryEffectTracerLayer } from '../src/tracer'; + +describe('SentryEffectTracerLayer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.effect('traces Effect spans to Sentry', () => + Effect.gen(function* () { + let capturedSpanName: string | undefined; + + yield* Effect.withSpan('test-parent-span')( + Effect.gen(function* () { + yield* Effect.annotateCurrentSpan('test-attribute', 'test-value'); + capturedSpanName = 'effect-span-executed'; + }), + ); + + expect(capturedSpanName).toBe('effect-span-executed'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('creates spans with correct attributes', () => + Effect.gen(function* () { + const result = yield* Effect.withSpan('my-operation')(Effect.succeed('success')); + + expect(result).toBe('success'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('handles nested spans', () => + Effect.gen(function* () { + const result = yield* Effect.withSpan('outer')( + Effect.gen(function* () { + const inner = yield* Effect.withSpan('inner')(Effect.succeed('inner-result')); + return `outer-${inner}`; + }), + ); + + expect(result).toBe('outer-inner-result'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('propagates span context through Effect fibers', () => + Effect.gen(function* () { + const results: string[] = []; + + yield* Effect.withSpan('parent')( + Effect.gen(function* () { + results.push('parent-start'); + yield* Effect.withSpan('child-1')(Effect.sync(() => results.push('child-1'))); + yield* Effect.withSpan('child-2')(Effect.sync(() => results.push('child-2'))); + results.push('parent-end'); + }), + ); + + expect(results).toEqual(['parent-start', 'child-1', 'child-2', 'parent-end']); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('handles span failures correctly', () => + Effect.gen(function* () { + const result = yield* Effect.withSpan('failing-span')(Effect.fail('expected-error')).pipe( + Effect.catchAll(e => Effect.succeed(`caught: ${e}`)), + ); + + expect(result).toBe('caught: expected-error'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('handles span with defects (die)', () => + Effect.gen(function* () { + const result = yield* Effect.withSpan('defect-span')(Effect.die('defect-value')).pipe( + Effect.catchAllDefect(d => Effect.succeed(`caught-defect: ${d}`)), + ); + + expect(result).toBe('caught-defect: defect-value'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('works with Effect.all for parallel operations', () => + Effect.gen(function* () { + const results = yield* Effect.withSpan('parallel-parent')( + Effect.all([ + Effect.withSpan('task-1')(Effect.succeed(1)), + Effect.withSpan('task-2')(Effect.succeed(2)), + Effect.withSpan('task-3')(Effect.succeed(3)), + ]), + ); + + expect(results).toEqual([1, 2, 3]); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('supports span annotations', () => + Effect.gen(function* () { + const result = yield* Effect.succeed('annotated').pipe( + Effect.withSpan('annotated-span'), + Effect.tap(() => Effect.annotateCurrentSpan('custom-key', 'custom-value')), + ); + + expect(result).toBe('annotated'); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets span status to ok on success', () => + Effect.gen(function* () { + const setStatusCalls: Array<{ code: number; message?: string }> = []; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(_options => { + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: (status: { code: number; message?: string }) => setStatusCalls.push(status), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('success-span')(Effect.succeed('ok')); + + expect(setStatusCalls).toContainEqual({ code: 1 }); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets span status to error on failure', () => + Effect.gen(function* () { + const setStatusCalls: Array<{ code: number; message?: string }> = []; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(_options => { + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: (status: { code: number; message?: string }) => setStatusCalls.push(status), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('error-span')(Effect.fail('test-error')).pipe(Effect.catchAll(() => Effect.void)); + + expect(setStatusCalls).toContainEqual({ code: 2, message: 'test-error' }); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets span status to error on defect', () => + Effect.gen(function* () { + const setStatusCalls: Array<{ code: number; message?: string }> = []; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(_options => { + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: (status: { code: number; message?: string }) => setStatusCalls.push(status), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('defect-span')(Effect.die('fatal-defect')).pipe(Effect.catchAllDefect(() => Effect.void)); + + expect(setStatusCalls).toContainEqual({ code: 2, message: 'fatal-defect' }); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('propagates Sentry span context via withActiveSpan', () => + Effect.gen(function* () { + const withActiveSpanCalls: sentryCore.Span[] = []; + + const mockWithActiveSpan = vi + .spyOn(sentryCore, 'withActiveSpan') + .mockImplementation((span: sentryCore.Span | null, callback: (scope: sentryCore.Scope) => T): T => { + if (span) { + withActiveSpanCalls.push(span); + } + return callback({} as sentryCore.Scope); + }); + + yield* Effect.withSpan('context-span')(Effect.succeed('done')); + + expect(withActiveSpanCalls.length).toBeGreaterThan(0); + + mockWithActiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets origin to auto.function.effect for regular spans', () => + Effect.gen(function* () { + let capturedAttributes: Record | undefined; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => { + capturedAttributes = options.attributes; + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: vi.fn(), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('my-operation')(Effect.succeed('ok')); + + expect(capturedAttributes).toBeDefined(); + expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.function.effect'); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets origin to auto.http.effect for http.server spans', () => + Effect.gen(function* () { + let capturedAttributes: Record | undefined; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => { + capturedAttributes = options.attributes; + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: vi.fn(), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('http.server GET /api/users')(Effect.succeed('ok')); + + expect(capturedAttributes).toBeDefined(); + expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('sets origin to auto.http.effect for http.client spans', () => + Effect.gen(function* () { + let capturedAttributes: Record | undefined; + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => { + capturedAttributes = options.attributes; + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: vi.fn(), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('http.client GET https://api.example.com')(Effect.succeed('ok')); + + expect(capturedAttributes).toBeDefined(); + expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); + + mockStartInactiveSpan.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); + + it.effect('uses transaction name from isolation scope for http.server spans', () => + Effect.gen(function* () { + let capturedSpanName: string | undefined; + + const mockGetIsolationScope = vi.spyOn(sentryCore, 'getIsolationScope').mockReturnValue({ + getScopeData: () => ({ + transactionName: 'GET /users/:id', + }), + } as unknown as sentryCore.Scope); + + const mockStartInactiveSpan = vi.spyOn(sentryCore, 'startInactiveSpan').mockImplementation(options => { + capturedSpanName = options.name; + return { + spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }), + isRecording: () => true, + setAttribute: vi.fn(), + setStatus: vi.fn(), + addEvent: vi.fn(), + end: vi.fn(), + } as unknown as sentryCore.Span; + }); + + yield* Effect.withSpan('http.server GET /users/123', { kind: 'server' })(Effect.succeed('ok')); + + expect(capturedSpanName).toBe('GET /users/:id'); + + mockStartInactiveSpan.mockRestore(); + mockGetIsolationScope.mockRestore(); + }).pipe(Effect.provide(SentryEffectTracerLayer)), + ); +}); diff --git a/packages/effect/tsconfig.json b/packages/effect/tsconfig.json new file mode 100644 index 000000000000..d49b053b37f8 --- /dev/null +++ b/packages/effect/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "build" + }, + "include": ["src/**/*"] +} diff --git a/packages/effect/tsconfig.test.json b/packages/effect/tsconfig.test.json new file mode 100644 index 000000000000..9dd90014ef37 --- /dev/null +++ b/packages/effect/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vitest.config.ts"], + + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/effect/tsconfig.types.json b/packages/effect/tsconfig.types.json new file mode 100644 index 000000000000..76eb1a9bb7c3 --- /dev/null +++ b/packages/effect/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + }, + "include": ["src/**/*"] +} diff --git a/packages/effect/tsconfig.vite.json b/packages/effect/tsconfig.vite.json new file mode 100644 index 000000000000..4f2b7371b076 --- /dev/null +++ b/packages/effect/tsconfig.vite.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + + "include": ["vite.config.ts", "vitest.config.ts"], + + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/effect/vitest.config.ts b/packages/effect/vitest.config.ts new file mode 100644 index 000000000000..ed62557713c1 --- /dev/null +++ b/packages/effect/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + include: ['test/**/*.test.ts'], + }, +}); diff --git a/yarn.lock b/yarn.lock index ac89a4468d6a..af2910e4f916 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3269,6 +3269,11 @@ dependencies: "@edge-runtime/primitives" "6.0.0" +"@effect/vitest@^0.23.9": + version "0.23.13" + resolved "https://registry.yarnpkg.com/@effect/vitest/-/vitest-0.23.13.tgz#17edf9d8e3443f080ff8fe93bd37b023612a07a4" + integrity sha512-F3x2phMXuVzqWexdcYp8v0z1qQHkKxp2UaHNbqZaEjPEp8FBz/iMwbi6iS/oIWzLfGF8XqdP8BGJptvGIJONNw== + "@ember-data/rfc395-data@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843" @@ -14877,6 +14882,14 @@ effect@3.16.12: "@standard-schema/spec" "^1.0.0" fast-check "^3.23.1" +effect@^3.19.19: + version "3.19.19" + resolved "https://registry.yarnpkg.com/effect/-/effect-3.19.19.tgz#643a5a4b7445cc924a28270bc6cd1a5c8facd27e" + integrity sha512-Yc8U/SVXo2dHnaP7zNBlAo83h/nzSJpi7vph6Hzyl4ulgMBIgPmz3UzOjb9sBgpFE00gC0iETR244sfXDNLHRg== + dependencies: + "@standard-schema/spec" "^1.0.0" + fast-check "^3.23.1" + ejs@^3.1.7: version "3.1.8" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" @@ -28096,7 +28109,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2"