diff --git a/.busted b/.busted index cec06bd..d39ce14 100644 --- a/.busted +++ b/.busted @@ -1,7 +1,6 @@ return { default = { - pattern = "_spec", ROOT = {"spec/"}, - lpath = "src/?.lua;src/?/init.lua;build/?.lua;build/?/init.lua" - } + lpath = "src/?.lua;src/?/init.lua;src/sentry/?.lua;src/sentry/?/init.lua" + }, } \ No newline at end of file diff --git a/.claude/commands/ldk/setup.md b/.claude/commands/ldk/setup.md deleted file mode 100644 index bcdfc81..0000000 --- a/.claude/commands/ldk/setup.md +++ /dev/null @@ -1,170 +0,0 @@ -# Setup Lua C/C++ Build System: $ARGUMENTS - -You will guide the user through setting up a complete Lua C/C++ extension build system for their project. - -## ⚠️ CRITICAL SECURITY AND SAFETY CONSTRAINTS - -**NEVER perform these actions:** -1. **Do NOT edit or modify files in `@.claude/ldk-resources/`** - These are templates that must remain unchanged -2. **Do NOT access files outside the current project directory** - Stay within the working directory -3. **Do NOT use sed/awk to edit template files directly** - Always copy first, then edit the copy -4. **Do NOT run git commands that modify the repository** - No git init, git add, git commit, git push, etc. -5. **Only use READ-ONLY git commands** - git rev-parse, git remote get-url, git config (for reading only) - -**Required workflow for templates:** -1. Copy template file to project directory using `cp @.claude/...` -2. Use Edit tool to modify the copied file (never the original template) - -**Prerequisites**: This command requires templates from the LDK resources directory (installed with this toolkit). - -## Step 1: Determine Package Name - -**If $ARGUMENTS contains a package name:** -1. Extract the package name from $ARGUMENTS -2. Say: "I'll set up build system for package '{package-name}'" -3. Ask: "Continue with this package name? (y/N)" - -**If $ARGUMENTS is empty OR user says no:** -1. Ask: "What's your package name? (e.g., mylib)" -2. Wait for user response and store as PACKAGE_NAME - -## Step 2: Gather Project Information - -**Detect Git status once and store results:** -1. Use Bash to check if current directory is a git repository root: - `[ "$(git rev-parse --show-toplevel 2>/dev/null)" = "$(pwd)" ] && echo "GIT_REPO" || echo "NOT_GIT"` -2. Store the result and proceed based on the detected status - -**If GIT_REPO detected:** -1. Get remote URL: `git remote get-url origin 2>/dev/null || echo "NO_REMOTE"` -2. Get maintainer info (prefer local, fallback to global): - - `git config --local user.name 2>/dev/null || git config --global user.name 2>/dev/null` - - `git config --local user.email 2>/dev/null || git config --global user.email 2>/dev/null` -3. Set maintainer information: - - Use git user.name if available - - Fallback to username part of email if name not available - - Default to "Your Name" if nothing available - - **Note**: Use name only, no email addresses for privacy -4. Use actual remote URL if available, otherwise use placeholder URLs with warning - -**If NOT_GIT detected:** -1. Use placeholder URLs: `https://example.com/[PACKAGE_NAME]` -2. Set maintainer to "Your Name" -3. **IMPORTANT**: Do NOT run any git commands - -## Step 3: Check for Existing Build Files - -Use the LS tool to check for existing build files: -- Check for: Makefile, makemk.lua, rockspecs/*.rockspec, *.rockspec - -**If any build files exist:** -1. List the found files to the user -2. Ask: "Found existing build files. Do you want to overwrite them? This will replace your current build configuration. (y/N)" -3. If user says no, respond: "Setup cancelled. Existing build files preserved." and stop. - -## Step 4: Set Up Build System Files - -### 4.1 Copy Core Files -Use the Bash tool to: -1. Copy makemk.lua: `cp @.claude/ldk-resources/makemk.lua .` -2. Copy Makefile template: `cp @.claude/ldk-resources/Makefile .` -3. Use Edit tool to customize the copied Makefile (substitute {{PACKAGE_NAME_UPPER}} with actual package name) - -### 4.2 Create Rockspec -1. Create rockspecs/ directory -2. Copy template: `cp @.claude/ldk-resources/template.rockspec rockspecs/$PACKAGE_NAME-dev-1.rockspec` -3. Use Edit tool to customize the copied rockspec with substitutions: - - {{PACKAGE_NAME}} → actual package name - - {{REPO_URL}} → determined source URL (from Step 2) - - {{HOMEPAGE_URL}} → determined homepage URL (from Step 2) - - {{MAINTAINER}} → determined maintainer name (from Step 2) - -### 4.3 Generate Smart Summary -**Analyze project context:** -1. Read CLAUDE.md if it exists for project description -2. Check README.md for project overview -3. Examine existing code structure and file names -4. Consider package name patterns and directory structure - -**Generate appropriate summary based on analysis:** -- For web frameworks: "A [lightweight/fast] web framework for Lua" -- For database drivers: "Lua bindings for [database] database" -- For parsers: "A [format] parser/library for Lua" -- For utilities: "[Package name] - [functional description] for Lua" -- Generic fallback: "A Lua library for [inferred purpose]" - -**Present summary to user:** -1. Say: "I've analyzed your project and generated this summary: '[GENERATED_SUMMARY]'" -2. Ask: "Use this summary for your rockspec? (Y/n)" -3. If user accepts, use Edit tool to update the copied rockspec with the generated summary -4. If user declines, ask: "Please provide your preferred package description:" and use Edit tool to update with their input - -## Step 5: Set Up Project Structure - -### 5.1 Check Existing Directories -Use LS tool to check for standard directories and report status: -- ✓ src/ directory exists (for C/C++ extensions) -- ✓ lua/ directory exists (for Lua modules) -- ✓ lib/ directory exists (for static libraries, optional) -- ✓ bin/ directory exists (for command scripts, optional) - -### 5.2 Offer Directory Creation -Ask: "Would you like me to create missing standard directories? (y/N)" - -**If yes:** -1. Create src/ and lua/ directories (core directories) -2. Ask separately: "Create lib/ directory for static libraries? (y/N)" -3. Ask separately: "Create bin/ directory for command scripts? (y/N)" -4. Create requested optional directories - -## Step 6: Verify and Summarize Setup - -**Confirm successful setup:** -1. Use LS to list created build files: Makefile, makemk.lua, rockspecs/ -2. Display package configuration summary: - - Package name - - Rockspec location - - Source URL - - Homepage URL - - Generated/custom summary - -## Step 7: Provide Final Guidance - -**Present completion message:** -"✅ Setup complete! Your build system now supports: -- Pure Lua projects (lua/ directory) -- C/C++ extensions (src/ directory) -- Static libraries (lib/ directory) -- Mixed-language projects -- Automatic compiler selection -- Source file directives (@cflags:, @ldflags:, @reflibs:, etc.) -- Coverage instrumentation - -**Next steps:** -1. **Add your code to the appropriate directories:** - - Lua modules → `lua/` - - C/C++ extensions → `src/` - - Static libraries → `lib/` - - Command scripts → `bin/` - -2. **Use source file directives in C/C++ files for automatic build configuration:** - ```c - //@cflags: -Wall -Werror - //@ldflags: -lm -lpthread - //@reflibs: string util/memory - ``` - -3. **Build and install your package:** - ```bash - luarocks make rockspecs/[PACKAGE_NAME]-dev-1.rockspec - ``` - -4. **For coverage analysis:** - ```bash - [PACKAGE_NAME_UPPER]_COVERAGE=1 luarocks make - ```" - -**Important notes:** -- If you used placeholder URLs, remind the user to update them in the rockspec -- The build system automatically detects C/C++ files and applies source directives -- See the LDK memory files for detailed usage examples diff --git a/.claude/ldk-resources/BUILD_SYSTEM_GUIDE.md b/.claude/ldk-resources/BUILD_SYSTEM_GUIDE.md deleted file mode 100644 index e89adc7..0000000 --- a/.claude/ldk-resources/BUILD_SYSTEM_GUIDE.md +++ /dev/null @@ -1,364 +0,0 @@ -# Lua Package Build System Technical Specification - -**Complete build system for Lua packages supporting pure Lua modules, C/C++ extensions, static libraries, command scripts, and mixed projects with LuaRocks integration and coverage support.** - -## Quick Start - -Use the Claude command for interactive setup: -``` -/commands/ldk/setup [package-name] -``` - -This command will: -- Detect your Git repository status -- Set up the complete build system -- Generate customized configuration files -- Create necessary directory structure - -## System Overview - -### Core Features -- **Complete Lua Package System** - Supports pure Lua modules, C/C++ extensions, static libraries, and command scripts -- **Git Submodule Support** - Automatic initialization and building of git submodules -- **Automatic File Discovery** - Scans `lua/`, `src/`, `lib/`, and `bin/` directories automatically -- **LuaRocks Integration** - Seamless integration with LuaRocks package management -- **Mixed Language Support** - Automatic handling of C and C++ files in same project -- **Static Library Support** - Build and link static libraries from `lib/` directory -- **Source Directives** - In-file configuration for compiler and linker flags -- **Coverage Support** - Build with gcov instrumentation for C/C++ code -- **Module Grouping** - Groups related source files by prefix into single modules -- **Automatic Compiler Selection** - Uses appropriate compiler based on file type - -### Build System Components - -1. **Makefile** - Main build configuration (generated from template) -2. **makemk.lua** - Module discovery and build rule generation script -3. **rockspec** - LuaRocks package specification (generated from template) -4. **mk/modules.mk** - Auto-generated module definitions (created during build) - -## Directory Structure - -The build system expects the following project structure: - -``` -your-project/ -├── lua/ # Pure Lua modules -│ ├── mypackage.lua # Main module (optional) -│ └── mypackage/ # Sub-modules (optional) -│ └── helper.lua -├── src/ # C/C++ extensions -│ ├── core.c # → core.so -│ ├── parser.cpp # → parser.so -│ └── utils/ # Nested modules -│ └── helper.c # → utils/helper.so -├── lib/ # Static libraries (optional) -│ ├── string.c # → libstring.a -│ └── util/ -│ └── memory.c # → libutil_memory.a -└── bin/ # Command scripts (optional) - └── mytool.lua # → executable command -``` - -## Source File Directives - -The build system supports in-file configuration through comment directives: - -```c -// example.c -//@cflags: -Wall -Werror -O2 -//@ldflags: -lm -lpthread -//@cppflags: -DENABLE_FEATURE -//@reflibs: string util/memory -``` - -### Available Directives -- **@cflags:** - Additional C compiler flags -- **@cxxflags:** - Additional C++ compiler flags -- **@cppflags:** - Preprocessor flags (both C and C++) -- **@ldflags:** - Linker flags and external libraries -- **@reflibs:** - Reference internal static libraries from lib/ - -## Git Submodule Support - -The build system provides automatic initialization and building of git submodules for projects that depend on external C/C++ libraries. - -### Submodule Auto-Initialization - -When using git submodules, the build system can automatically initialize them before building your project: - -```bash -# The build system checks for: -# 1. Current directory is git repository root -# 2. .gitmodules file exists -# 3. Uninitialized submodules are present -# Then automatically runs: git submodule update --init --recursive -``` - -### Submodule Building - -To build submodule dependencies, uncomment and customize the relevant lines in your Makefile: - -```makefile -submodule-deps: - # If using git submodules, uncomment the following line to auto-initialize: - $(MAKE) submodule-init - - # Build submodule dependencies with isolated environment: - env -i \ - PATH="$$PATH" \ - HOME="$$HOME" \ - SHELL="$$SHELL" \ - USER="$$USER" \ - LANG="$$LANG" \ - LC_ALL="$$LC_ALL" \ - $(MAKE) -C deps/somelib OPTION=value target.a -``` - -### Environment Isolation - -Submodule builds use environment isolation to prevent interference from parent project variables: - -- **Cleared Variables**: All environment variables are cleared with `env -i` -- **Preserved Variables**: Only essential variables are inherited: - - `PATH` - For finding tools and compilers - - `HOME` - For user configuration - - `SHELL` - For proper shell execution - - `USER` - For user identification - - `LANG`, `LC_ALL` - For proper message localization - -### Example Workflow - -1. **Add Submodule**: - ```bash - git submodule add https://github.com/example/somelib.git deps/somelib - ``` - -2. **Configure Build**: Uncomment and customize the build commands in `submodule-deps` target - -3. **Build Project**: The build system will automatically: - - Initialize submodules if needed - - Build submodule dependencies with isolated environment - - Build main project modules - -### Error Handling - -The build system provides clear error messages for common issues: - -- **Not in git root**: `ERROR: Not in git repository root` -- **Missing .gitmodules**: `ERROR: No .gitmodules file found. Add submodules first with 'git submodule add'` -- **Build failures**: Submodule build errors automatically stop the main build - -## Module Types and Compilation - -### Pure Lua Package -Only `lua/` directory with `.lua` files - no compilation needed - -### C/C++ Extension Package -Source files in `src/` directory: -- **C Files (.c)**: Compiled with `$(CC)` (typically gcc/clang) -- **C++ Files (.cpp)**: Compiled with `$(CXX)` (typically g++/clang++) -- **Output**: Shared libraries (.so files) installed to Lua C module path - -### Static Libraries -Source files in `lib/` directory: -- Built as `.a` files for internal linking -- Referenced by C/C++ extensions via `@reflibs:` directive -- Support nested directory structure with prefix grouping - -### Mixed Package -Combination of Lua modules, C/C++ extensions, and static libraries - -### Command Scripts -Executable Lua scripts in `bin/` directory - installed to system bin path - -## Module Grouping Rules - -The build system automatically groups related source files: - -### Prefix Grouping -Files with common prefix are grouped into single module: -``` -src/ -├── network.c → All grouped into network.so -├── network_client.c → -└── network_server.cpp → (links with -lstdc++) -``` - -### Directory Modules -Files in subdirectories create namespaced modules: -``` -src/ -└── utils/ - ├── string.c → utils/string.so - └── memory.c → utils/memory.so -``` - -### Static Library Grouping -Similar prefix grouping for static libraries: -``` -lib/ -├── string.c → libstring.a -├── string_ops.c → (grouped into libstring.a) -└── util/ - └── memory.c → libutil_memory.a -``` - -## Build Commands - -### Basic Operations -```bash -# Build and install package -luarocks make rockspecs/[package]-dev-1.rockspec - -# Build with coverage instrumentation -[PACKAGE]_COVERAGE=1 luarocks make - -# Clean build artifacts -make clean - -# Show build configuration -make show-config - -# Initialize git submodules only -make submodule-init - -# Build submodule dependencies only -make submodule-deps -``` - -### Development Workflow -```bash -# Initial setup -/commands/ldk/setup mypackage - -# Add git submodules (if needed) -git submodule add https://github.com/example/library.git deps/library - -# Configure submodule builds in Makefile's submodule-deps target - -# Development cycle -luarocks make # Build and install package (includes submodules) -make clean # Clean artifacts (optional) -``` - -## Coverage Analysis - -Enable coverage instrumentation by setting environment variable: -```bash -MYPACKAGE_COVERAGE=1 luarocks make -``` - -This enables: -- gcov instrumentation for C/C++ code -- Coverage data collection during tests -- Report generation with gcov/lcov tools - -## Template Variables - -The build system uses these template placeholders: - -| Variable | Description | Example | -|----------|-------------|---------| -| `{{PACKAGE_NAME}}` | Package name | mylib | -| `{{PACKAGE_NAME_UPPER}}` | Uppercase for env vars | MYLIB | -| `{{REPO_URL}}` | Repository source URL | https://github.com/user/repo | -| `{{HOMEPAGE_URL}}` | Project homepage | https://github.com/user/repo | -| `{{MAINTAINER}}` | Package maintainer name | John Doe | - -## Requirements - -### System Requirements -- LuaRocks for Lua package management -- GCC or compatible C compiler -- G++ or compatible C++ compiler (for C++ extensions) -- Standard Unix tools (make, install, find, etc.) - -### Compiler Configuration -- **C Compiler**: Uses `$(CC)` from LuaRocks environment -- **C++ Compiler**: Derives from `$(CC)` (gcc→g++, clang→clang++) -- **Flags**: Inherits SDK and platform settings from LuaRocks - -## Advanced Features - -### Cross-Platform Support -- Automatic detection of platform-specific settings -- Proper library extensions (.so, .dll, .dylib) -- Compatible with macOS, Linux, and BSD systems - -### Parallel Builds -Supports parallel compilation with `make -j` for faster builds - -### Dynamic Module Discovery -The makemk.lua script automatically: -- Scans source directories for modules -- Generates build rules dynamically -- Handles complex dependency resolution - -## Manual Setup Reference - -If you need to set up files manually (not recommended), here's the basic process: - -1. Copy template files to project root: - - `.claude/ldk-resources/Makefile` → `Makefile` - - `.claude/ldk-resources/makemk.lua` → `makemk.lua` - - `.claude/ldk-resources/template.rockspec` → `rockspecs/[package]-dev-1.rockspec` - -2. Replace template variables in copied files: - - `{{PACKAGE_NAME}}` with your package name - - `{{PACKAGE_NAME_UPPER}}` with uppercase package name - - `{{REPO_URL}}` with repository URL - - `{{HOMEPAGE_URL}}` with project homepage - - `{{MAINTAINER}}` with maintainer name - -3. Create directory structure as needed: - ```bash - mkdir -p lua src lib bin rockspecs - ``` - -**Note**: Using `/commands/ldk/setup` is strongly recommended as it handles all these steps automatically and correctly. - -## Related Documentation - -- **Memory Files**: `.claude/memories/ldk-*.md` - Development guidelines and patterns -- **Commands**: `.claude/commands/ldk/` - Interactive setup procedures - -## Troubleshooting - -### Common Issues - -**Module not found after installation** -- Ensure `luarocks make` completed successfully -- Check LUA_PATH and LUA_CPATH environment variables - -**C extension compilation fails** -- Verify compiler is installed and accessible -- Check source file directives for syntax errors -- Review compiler output for missing dependencies - -**Static library linking errors** -- Ensure referenced libraries exist in lib/ directory -- Check @reflibs: directive spelling and paths -- Verify library source files compile successfully - -**Coverage build fails** -- Confirm gcov is installed -- Check that package name environment variable is uppercase -- Verify compiler supports coverage flags - -**Git submodule issues** -- Ensure you're in the git repository root directory -- Verify .gitmodules file exists and is properly configured -- Check that submodules are accessible and can be cloned -- Confirm submodule build commands are properly uncommented in Makefile - -**Submodule build fails** -- Check that the submodule's build system works independently -- Verify environment isolation isn't preventing required tools access -- Review submodule documentation for specific build requirements - -### Getting Help - -For issues or questions: -1. Check the memory files in `.claude/memories/` for patterns and guidelines -2. Review this technical specification -3. Use Claude to diagnose and fix issues with your build configuration diff --git a/.claude/ldk-resources/Makefile b/.claude/ldk-resources/Makefile deleted file mode 100644 index 0916fb1..0000000 --- a/.claude/ldk-resources/Makefile +++ /dev/null @@ -1,371 +0,0 @@ -## -# Lua Package Build System with Automatic Module Discovery -## -# -# PURPOSE: -# This Makefile provides an automated build system for Lua packages that -# contain mixed Lua (.lua) and C/C++ (.c/.cpp) modules. It automatically -# discovers source files and builds them into the appropriate Lua modules. -# -# WHY THIS DESIGN: -# - Traditional Makefiles require manual listing of every source file -# - Module names must match directory structure for Lua's require() system -# - C modules need proper luaopen_* function naming conventions -# - Installation paths must be correct for luarocks package management -# -# KEY FEATURES: -# 1. AUTOMATIC DISCOVERY: Scans src/, lua/, bin/ directories automatically -# 2. PREFIX GROUPING: Groups C files by prefix (foo.c + foo_bar.c → single module) -# 3. NESTED MODULES: Supports directory hierarchy (src/util/parser.c → util.parser) -# 4. MIXED LANGUAGES: Handles C, C++, and Lua files in same project -# 5. LUAROCKS INTEGRATION: Designed to work seamlessly with luarocks build system -# 6. COVERAGE SUPPORT: Built-in support for code coverage analysis -# -# BENEFITS: -# - Zero configuration: Just add files to src/, lua/, bin/ and they're built -# - Maintainable: No need to update Makefile when adding/removing source files -# - Consistent: Enforces proper Lua module naming and structure conventions -# - Portable: Works across different platforms via luarocks -# -# USAGE: -# This Makefile is designed to be used exclusively through 'luarocks make'. -# Direct 'make' execution is prevented to ensure proper variable setup. -# -# MODULE VARIABLE DOCUMENTATION: -# The modules.mk file (generated by makemk.lua) creates variables for each C module: -# -# PREFIX GROUPING: makemk.lua groups files by prefix within each directory -# src/foo.c, src/foo_bar.c, src/foo_baz.c → single 'foo' module -# src/bar.c, src/baz.c → separate 'bar' and 'baz' modules -# -# For each module with grouped files, e.g., src/foo.c + src/foo_bar.c + src/foo_baz.c: -# - Path conversion: src/foo -> foo (/ becomes _) -# - Generated variables: -# * foo_SRC = src/foo.c src/foo_bar.c src/foo_baz.c -# * foo_OBJS = $(foo_SRC:.c=.o) # Object files (auto-generated from SRC) -# := $(foo_OBJS:.cpp=.o) # Also handles .cpp files -# * foo_LINKER = $(CC) or $(CXX) # Linker command (CC for C, CXX for C++) -# * foo_LDFLAGS = [flags] # Additional linker flags (e.g., -lstdc++) -# - Lua module: require('.foo') -# -# Nested directory example, e.g., src/foo/bar.c: -# - Path conversion: src/foo/bar -> foo_bar (/ becomes _) -# - Generated variables: -# * foo_bar_SRC = src/foo/bar.c # Single source file in subdirectory -# * foo_bar_OBJS = $(foo_bar_SRC:.c=.o) # Single object file -# * foo_bar_LINKER = $(CC) # Linker command -# * foo_bar_LDFLAGS = # No additional linker flags -# - Lua module: require('.foo.bar') -# -# Nested directory with grouping, e.g., src/foo/qux.c + src/foo/qux_helper.c: -# - Path conversion: src/foo/qux -> foo_qux (/ becomes _) -# - Generated variables (multiple files grouped into single nested module): -# * foo_qux_SRC = src/foo/qux.c src/foo/qux_helper.c -# * foo_qux_OBJS = $(foo_qux_SRC:.c=.o) # Multiple object files -# * foo_qux_LINKER = $(CC) # Linker command -# * foo_qux_LDFLAGS = # No additional linker flags -# - Lua module: require('.foo.qux') -# -# Additionally, modules.mk defines: -# MODULES = src/foo src/foo/bar src/foo/qux # List of all discovered modules -# -# These variables are used by the static pattern rules: -# $(MODULE_TARGETS): %.$(LIB_EXTENSION): $(...) -# -# Variable naming transformations: -# foo.so -> foo_OBJS, foo_LINKER, foo_LDFLAGS -# foo/bar.so -> foo_bar_OBJS, foo_bar_LINKER, foo_bar_LDFLAGS -# foo/qux.so -> foo_qux_OBJS, foo_qux_LINKER, foo_qux_LDFLAGS -# - -# Build flags configuration -# Coverage flags (enabled when {{PACKAGE_NAME_UPPER}}_COVERAGE is set) -ifdef {{PACKAGE_NAME_UPPER}}_COVERAGE -COVFLAGS = --coverage -endif - - -## -# Display build environment information -# These variables are provided by luarocks via rockspec build_variables -## -define DISPLAY_BUILD_ENV -$(info ========================================================================) -$(info Phase 1: Build Environment Setup) -$(info ========================================================================) -$(info External Variables from luarocks:) -# Package name (e.g., "example") -$(info PACKAGE_NAME: $(PACKAGE_NAME)) -# Library extension (e.g., "so" on Linux/macOS, "dll" on Windows) -$(info LIB_EXTENSION: $(LIB_EXTENSION)) -# Lua module installation directory (e.g., /usr/local/share/lua/5.1) -$(info LUADIR: $(LUADIR)) -# C library installation directory (e.g., /usr/local/lib/lua/5.1) -$(info LIBDIR: $(LIBDIR)) -# Binary/script installation directory (e.g., /usr/local/bin) -$(info BINDIR: $(BINDIR)) -# C compiler command (e.g., "gcc", "clang") -$(info CC: $(CC)) -# C++ compiler command (e.g., "g++", "clang++") -$(info CXX (derived): $(CXX)) -# C compiler flags -$(info CFLAGS: $(CFLAGS)) -# C++ compiler flags -$(info CXXFLAGS: $(CXXFLAGS)) -# Linker flags -$(info LDFLAGS: $(LDFLAGS)) -# Platform-specific linker flags -$(info PLATFORM_LDFLAGS: $(PLATFORM_LDFLAGS)) -# Coverage flags (enabled when {{PACKAGE_NAME_UPPER}}_COVERAGE is set) -$(info COVFLAGS: $(COVFLAGS)) -$(info ========================================================================) -endef - -# Display build environment information only when not running clean -ifeq ($(filter clean,$(MAKECMDGOALS)),) -$(eval $(call DISPLAY_BUILD_ENV)) -endif - -## -# External variable validation -## -# Allow 'make clean' to run without PACKAGE_NAME -ifeq ($(filter clean,$(MAKECMDGOALS)),) - ifndef PACKAGE_NAME - $(error This Makefile must be used through 'luarocks make'. Please run 'luarocks make' instead of 'make' directly.) - endif -endif - -# Validate required external variables (skip for clean target) -ifeq ($(filter clean,$(MAKECMDGOALS)),) - $(info Validating required external variables...) - ifndef LIB_EXTENSION - $(error Required variable LIB_EXTENSION is not set. Check rockspec install_variables.) - endif -endif - - -## -# Input file discovery and processing -## -# Discover command files in bin/ directory -ifeq ($(filter clean,$(MAKECMDGOALS)),) -$(info Discovering command scripts in bin/ directory...) -endif -CMD_SOURCES = $(shell find bin -name '*.lua' 2>/dev/null || true) -CMD_TARGETS = $(patsubst bin/%.lua, $(BINDIR)/%, $(CMD_SOURCES)) - -# Discover Lua module files in lua/ directory -ifeq ($(filter clean,$(MAKECMDGOALS)),) -$(info Discovering Lua modules in lua/ directory...) -endif -LUASRCS = $(shell find lua -name '*.lua' 2>/dev/null || true) -LUALIBS = $(patsubst lua/%,$(LUALIBDIR)/%,$(filter-out lua/$(PACKAGE_NAME).lua,$(LUASRCS))) -# Set MAINLIB for either Lua or C main module (but not both) -MAINLIB = $(if $(wildcard lua/$(PACKAGE_NAME).lua),$(LUADIR)/$(PACKAGE_NAME).lua,$(if $(filter $(PACKAGE_NAME),$(MODULE_NAMES)),$(LIBDIR)/$(PACKAGE_NAME).$(LIB_EXTENSION))) - -## -# C module definition generation and processing -# makemk.lua scans src/ directory and groups C/C++ files by prefix to create modules -## -ifeq ($(filter install clean,$(MAKECMDGOALS)),) -$(info ========================================================================) -$(info Phase 2: C Module Discovery and Processing) -$(info ========================================================================) -$(info Scanning src/ directory for C/C++ source files...) -MAKEMK_OUTPUT := $(shell lua makemk.lua >&2 && echo "SUCCESS" || echo "FAILED") -ifneq ($(MAKEMK_OUTPUT),SUCCESS) -$(error makemk.lua failed to generate mk/modules.mk. Check the error messages above.) -endif -endif - -# Include module definitions from mk/modules.mk (skip for clean target) -ifeq ($(filter clean,$(MAKECMDGOALS)),) - $(info Loading generated module definitions from mk/modules.mk...) - -include mk/modules.mk -endif - -# Generate target variables and paths from C module definitions -ifeq ($(filter clean,$(MAKECMDGOALS)),) -$(info Generating build targets and installation paths...) -endif -# MODULE_NAMES: extract just the filename (util, helper) -MODULE_NAMES = $(foreach mod,$(MODULES),$(notdir $(mod))) -# MODULE_TARGETS: convert src/util/helper -> src/util/helper.$(LIB_EXTENSION) (same directory as .o files) -MODULE_TARGETS = $(addsuffix .$(LIB_EXTENSION),$(MODULES)) -# MODULE_LIBS: full installation paths for C libraries (excluding main module if it's C) -MODULE_LIBS = $(patsubst src/%,$(CLIBDIR)/%,$(addsuffix .$(LIB_EXTENSION),$(filter-out src/$(PACKAGE_NAME),$(MODULES)))) - -## -# Configuration and validation -## -ifeq ($(filter clean,$(MAKECMDGOALS)),) -$(info Configuring build environment and validating module setup...) -endif -# Installation directory configuration -# LUALIBDIR - Package-specific Lua library directory ($(LUADIR)/$(PACKAGE_NAME)) -# CLIBDIR - Package-specific C library directory ($(LIBDIR)/$(PACKAGE_NAME)) -LUALIBDIR = $(LUADIR)/$(PACKAGE_NAME) -CLIBDIR = $(LIBDIR)/$(PACKAGE_NAME) - -# Module validation -# Check for main module conflicts - prevent having both lua/package.lua and src/package.c -HAS_MAIN_LUA = $(wildcard lua/$(PACKAGE_NAME).lua) -HAS_MAIN_C = $(filter $(PACKAGE_NAME),$(MODULE_NAMES)) - -ifneq ($(HAS_MAIN_LUA),) -ifneq ($(HAS_MAIN_C),) -$(error Error: Both Lua main module (lua/$(PACKAGE_NAME).lua) and C main module (src/$(PACKAGE_NAME).c) exist. Please use only one main module type.) -endif -endif - -## -# Installation macro -## -# Common installation macro - creates directory and copies file -define INSTALL_FILES - @echo "Installing $< -> $@" - @echo "Creating directory: $(@D)" - @mkdir -p "$(@D)" - @install "$<" "$@" -endef - - -## -# Build targets -## -.PHONY: all submodule-init submodule-deps install install-commands install-lualibs clean show-vars - -# Default target - build all C modules -all: submodule-deps $(MODULE_TARGETS) - - -## -# Git submodule auto-initialization target -## -submodule-init: - @# Check if we're in a git repository root - @if [ "$$(git rev-parse --show-toplevel 2>/dev/null)" != "$$(pwd)" ]; then \ - echo "ERROR: Not in git repository root"; \ - exit 1; \ - fi - @# Check if .gitmodules exists - @if [ ! -f .gitmodules ]; then \ - echo "ERROR: No .gitmodules file found. Add submodules first with 'git submodule add'"; \ - exit 1; \ - fi - @# Check if there are uninitialized submodules - @if [ -z "$$(git submodule status 2>/dev/null | grep '^-' | head -1)" ]; then \ - echo "All submodules already initialized"; \ - exit 0; \ - fi - @echo "========================================================================" - @echo "Git submodules detected but not initialized. Initializing now..." - @echo "========================================================================" - @git submodule update --init --recursive - - -## -# Submodule dependency target -## -submodule-deps: - # - # If using git submodules, uncomment the following line to auto-initialize: - # - # $(MAKE) submodule-init - # - # Build submodule dependencies (User-defined) - # Use env -i to clear environment and only inherit the following variables: - # - # env -i \ - # PATH="$$PATH" \ - # HOME="$$HOME" \ - # SHELL="$$SHELL" \ - # USER="$$USER" \ - # LANG="$$LANG" \ - # LC_ALL="$$LC_ALL" \ - # $(MAKE) -C - -## -# Installation rules -## -# Main module installation (either Lua or C) -ifneq ($(MAINLIB),) -# Install Lua main module if it exists -ifneq ($(wildcard lua/$(PACKAGE_NAME).lua),) -$(MAINLIB): lua/$(PACKAGE_NAME).lua - $(INSTALL_FILES) -else -# Install C main module if it exists -$(MAINLIB): src/$(PACKAGE_NAME).$(LIB_EXTENSION) - $(INSTALL_FILES) -endif -endif - -# C library files installation (src/*.so -> $(CLIBDIR)/*.so) -# Pattern matches entire path structure from build to installation -$(CLIBDIR)/%.$(LIB_EXTENSION): src/%.$(LIB_EXTENSION) - $(INSTALL_FILES) - -# Command script installation (bin/*.lua -> $(BINDIR)/*) -$(BINDIR)/%: bin/%.lua - $(INSTALL_FILES) - @chmod +x $@ - -# Command installation target -install-commands: $(CMD_TARGETS) - -# Lua library files installation (lua/*.lua -> $(LUALIBDIR)/*) -$(LUALIBDIR)/%: lua/% - $(INSTALL_FILES) - -# Lua library installation target -install-lualibs: $(LUALIBS) - -# Main installation target - installs all discovered files -install: submodule-deps install-commands install-lualibs $(MAINLIB) $(MODULE_LIBS) - @echo "" - @echo "DEBUG: Install phase environment variables:" - @echo "LUADIR: $(LUADIR)" - @echo "LIBDIR: $(LIBDIR)" - @echo "BINDIR: $(BINDIR)" - @echo "" - @echo "Installation complete" - @$(MAKE) clean-objects - -## -# Debug and utility targets -## -# Show all build variables - useful for debugging and verification -show-vars: - @echo "=== External Variables ===" - @echo "PACKAGE_NAME: $(PACKAGE_NAME)" - @echo "LIB_EXTENSION: $(LIB_EXTENSION)" - @echo "LUADIR: $(LUADIR)" - @echo "LIBDIR: $(LIBDIR)" - @echo "BINDIR: $(BINDIR)" - @echo "" - @echo "=== Input Files ===" - @echo "CMD_SOURCES: $(CMD_SOURCES)" - @echo "LUASRCS: $(LUASRCS)" - @echo "MODULES: $(MODULES)" - @echo "" - @echo "=== Generated Targets ===" - @echo "CMD_TARGETS: $(CMD_TARGETS)" - @echo "LUALIBS: $(LUALIBS)" - @echo "MAINLIB: $(MAINLIB)" - @echo "MODULE_NAMES: $(MODULE_NAMES)" - @echo "MODULE_TARGETS: $(MODULE_TARGETS)" - @echo "MODULE_LIBS: $(MODULE_LIBS)" - -# Clean target - remove build artifacts -clean-objects: - # Remove all libraries (.so/.dll) and object files - find src \( -name "*.so" -o -name "*.dll" -o -name "*.o" \) -delete 2>/dev/null || true - find lib \( -name "*.a" -o -name "*.o" \) -delete 2>/dev/null || true - -clean: - # Remove all coverage artifacts - find src \( -name "*.gcda" -o -name "*.gcno" \) -delete 2>/dev/null || true - find lib \( -name "*.gcda" -o -name "*.gcno" \) -delete 2>/dev/null || true - @$(MAKE) clean-objects diff --git a/.claude/ldk-resources/README.md b/.claude/ldk-resources/README.md deleted file mode 100644 index 35710dd..0000000 --- a/.claude/ldk-resources/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Lua Resources Directory - -**Template files and configuration resources for Lua development tools.** - -## Purpose - -The `ldk-resources/` directory contains Lua-specific template files, configuration examples, and other resources that can be copied and customized for Lua development tools. These files are referenced by setup commands in the `commands/ldk/` directory. - -## Available Resources - -### Lua Package Build System -- **Files**: `Makefile`, `makemk.lua`, `template.rockspec`, `setup-makefile.sh` -- **Purpose**: Complete Lua package build system supporting pure Lua modules, C/C++ extensions, git submodules, and mixed projects -- **Features**: Automatic file discovery, LuaRocks integration, submodule support, environment isolation -- **Setup**: Use Claude command `/commands/ldk/setup [package] [user]` for setup -- **Documentation**: See `BUILD_SYSTEM_GUIDE.md` for detailed information - -## Design Philosophy - -- **Resource-Focused** - Contains only files and templates -- **Copy-and-Customize** - Templates provide starting points -- **Lua-Specific** - Optimized for Lua development workflows -- **Version-Controlled** - Templates evolve with best practices - -## Usage Pattern - -1. Identify needed resources from available templates -2. Follow setup commands in `commands/ldk/` directory -3. Copy template files to your project -4. Customize templates for your specific project needs -5. Maintain local customizations as needed - -## Relationship to Commands - -- **`commands/ldk/`** - How to use these resources (procedures) -- **`ldk-resources/`** - What to use (template files) -- **`memories/`** - Why and when to use tools (guidelines) - -This separation keeps template files organized and easily accessible while maintaining clear setup procedures. \ No newline at end of file diff --git a/.claude/ldk-resources/setup-makefile.sh b/.claude/ldk-resources/setup-makefile.sh deleted file mode 100755 index 682e0b7..0000000 --- a/.claude/ldk-resources/setup-makefile.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/bin/bash -# Lua C/C++ Extension Build System Setup Script -# Generates customized Makefile, makemk.lua, and rockspec - -set -e - -# Check arguments -if [ $# -ne 2 ]; then - echo "Usage: $0 " - echo "Example: $0 mylib johndoe" - exit 1 -fi - -PACKAGE_NAME="$1" -GITHUB_USER="$2" - -# Check for help flag -if [[ "$1" == "--help" || "$1" == "-h" ]]; then - echo "Usage: $0 " - echo "Example: $0 mylib johndoe" - echo - echo "Generates customized Lua C/C++ build system files:" - echo " - Makefile with coverage support" - echo " - makemk.lua module discovery script" - echo " - -dev-1.rockspec for LuaRocks" - exit 0 -fi - -# Validate inputs -if [ -z "$PACKAGE_NAME" ]; then - echo "Error: Package name cannot be empty" - exit 1 -fi - -if [ -z "$GITHUB_USER" ]; then - echo "Error: GitHub username cannot be empty" - exit 1 -fi - -# Convert package name to uppercase for coverage flags -PACKAGE_NAME_UPPER=$(echo "$PACKAGE_NAME" | tr '[:lower:]' '[:upper:]') - -echo "=== Lua Makefile Setup ===" -echo "Package name: $PACKAGE_NAME" -echo "GitHub user: $GITHUB_USER" -echo "Coverage flag: ${PACKAGE_NAME_UPPER}_COVERAGE" -echo - -# Check if we're in the template directory -if [ ! -f ".claude/ldk-resources/Makefile" ]; then - echo "Error: .claude/ldk-resources/Makefile not found. Make sure you're in the correct directory." - exit 1 -fi - -# Check for existing build files -EXISTING_FILES="" -[ -f "Makefile" ] && EXISTING_FILES="$EXISTING_FILES Makefile" -[ -f "makemk.lua" ] && EXISTING_FILES="$EXISTING_FILES makemk.lua" -[ -f "rockspecs/$PACKAGE_NAME-dev-1.rockspec" ] && EXISTING_FILES="$EXISTING_FILES rockspecs/$PACKAGE_NAME-dev-1.rockspec" -[ -f "$PACKAGE_NAME-dev-1.rockspec" ] && EXISTING_FILES="$EXISTING_FILES $PACKAGE_NAME-dev-1.rockspec" - -if [ -n "$EXISTING_FILES" ]; then - echo "Warning: Found existing build files:$EXISTING_FILES" - echo "These files will be overwritten." - echo -fi - -echo "Generating files..." - -# Generate customized Makefile -sed -e "s/{{PACKAGE_NAME}}/$PACKAGE_NAME/g" \ - -e "s/{{PACKAGE_NAME_UPPER}}/$PACKAGE_NAME_UPPER/g" \ - .claude/ldk-resources/Makefile > Makefile -echo "✓ Makefile generated" - -# Copy makemk.lua (no customization needed) -cp .claude/ldk-resources/makemk.lua . -echo "✓ makemk.lua copied" - -# Create rockspecs directory if it doesn't exist -mkdir -p rockspecs - -# Generate customized rockspec in rockspecs directory -sed -e "s/{{PACKAGE_NAME}}/$PACKAGE_NAME/g" \ - -e "s/{{GITHUB_USER}}/$GITHUB_USER/g" \ - .claude/ldk-resources/template.rockspec > "rockspecs/$PACKAGE_NAME-dev-1.rockspec" -echo "✓ rockspecs/$PACKAGE_NAME-dev-1.rockspec generated" - -echo -echo "Setup complete! Your C/C++ build system is ready." -echo -echo "Directory structure expected:" -echo " rockspecs/ - Rockspec files (created)" -echo " src/ - C/C++ source files (.c, .cpp)" -echo " lua/ - Lua library files (.lua)" -echo " bin/ - Command scripts (.lua, optional)" -echo -echo "Usage:" -echo " luarocks make rockspecs/$PACKAGE_NAME-dev-1.rockspec # Build and install" -echo " ${PACKAGE_NAME_UPPER}_COVERAGE=1 luarocks make rockspecs/$PACKAGE_NAME-dev-1.rockspec # Build with coverage" -echo -echo "Supports:" -echo " - Pure Lua projects (lua/ only)" -echo " - Pure C projects (.c files)" -echo " - Pure C++ projects (.cpp files)" -echo " - Mixed C/C++ projects" -echo " - Mixed Lua and C/C++ projects" -echo " - Automatic compiler selection" -echo " - File grouping by prefix" -echo " - Coverage instrumentation" diff --git a/.claude/ldk-resources/template.rockspec b/.claude/ldk-resources/template.rockspec deleted file mode 100644 index 572e924..0000000 --- a/.claude/ldk-resources/template.rockspec +++ /dev/null @@ -1,28 +0,0 @@ -package = "{{PACKAGE_NAME}}" -version = "dev-1" -source = {url = "git+{{REPO_URL}}.git"} -description = { - summary = "A Lua package built with claude-lua-devkit", - homepage = "{{HOMEPAGE_URL}}", - license = "MIT/X11", - maintainer = "{{MAINTAINER}}" -} -dependencies = {"lua >= 5.1"} -build = { - type = 'make', - build_variables = { - PACKAGE_NAME = "{{PACKAGE_NAME}}", - LIB_EXTENSION = "$(LIB_EXTENSION)", - CFLAGS = "$(CFLAGS)", - CPPFLAGS = "-I$(LUA_INCDIR)", - LDFLAGS = "$(LIBFLAG)", - WARNINGS = "-Wall -Wno-trigraphs -Wmissing-field-initializers -Wreturn-type -Wmissing-braces -Wparentheses -Wno-switch -Wunused-function -Wunused-label -Wunused-parameter -Wunused-variable -Wunused-value -Wuninitialized -Wunknown-pragmas -Wshadow -Wsign-compare" - }, - install_variables = { - PACKAGE_NAME = "{{PACKAGE_NAME}}", - LIB_EXTENSION = "$(LIB_EXTENSION)", - BINDIR = "$(BINDIR)", - LIBDIR = "$(LIBDIR)", - LUADIR = "$(LUADIR)" - } -} diff --git a/.claude/ldk-resources/version-template.md b/.claude/ldk-resources/version-template.md deleted file mode 100644 index e7699a2..0000000 --- a/.claude/ldk-resources/version-template.md +++ /dev/null @@ -1,133 +0,0 @@ -# Version Management: $ARGUMENTS -# INSTALL_VERSION=development -# INSTALL_COMMIT=HEAD -# INSTALL_DATE=Not installed via INSTALL.md -# REPOSITORY=https://github.com/mah0x211/claude-lua-devkit.git - -You will manage the claude-lua-devkit installation version. - -## Step 1: Parse Arguments - -Parse $ARGUMENTS: -- IF empty or "current": Show current version information -- IF "list" or "ls": Show available remote versions -- IF "update": Update to latest version using remote INSTALL.md -- IF "update [version]": Update to specific version -- IF "help": Show usage information - -## Step 2: Execute Command Based on Arguments - -### Show Current Version (default, "current") - -Extract version information from this command file: -```bash -CURRENT_VERSION=$(grep "^# INSTALL_VERSION=" "$0" | cut -d'=' -f2) -CURRENT_COMMIT=$(grep "^# INSTALL_COMMIT=" "$0" | cut -d'=' -f2) -CURRENT_DATE=$(grep "^# INSTALL_DATE=" "$0" | cut -d'=' -f2-) -CURRENT_REPO=$(grep "^# REPOSITORY=" "$0" | cut -d'=' -f2-) -``` - -Display version information nicely: -``` -claude-lua-devkit Version Information: -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Version: $CURRENT_VERSION -Commit: $CURRENT_COMMIT -Installed: $CURRENT_DATE -Repository: $CURRENT_REPO -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -``` - -### List Remote Versions ("list", "ls") - -Say: "Fetching available versions from GitHub..." - -```bash -# Create temporary directory for version check -mkdir -p .tmp -git clone --bare https://github.com/mah0x211/claude-lua-devkit.git .tmp/version-check 2>/dev/null -``` - -IF clone successful: -```bash -cd .tmp/version-check -git tag -l 'v*' | sort -V -cd ../.. -rm -rf .tmp/version-check -``` - -Display available versions: -``` -Available claude-lua-devkit versions: -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -[list of versions] -master (development) -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -To update: /commands/ldk/version update [version] -Example: /commands/ldk/version update v1.0.0 -``` - -IF clone failed: -Say: "Failed to fetch version information. Please check your internet connection." - -### Update Installation ("update", "update [version]") - -IF specific version provided: -- Say: "Updating to version [version]..." -- Set target version for installation - -IF no version provided: -- Say: "Updating to latest version..." -- Set target to master/latest - -Execute update using remote INSTALL.md: -Say: "Executing update using remote installation procedure..." - -```bash -# Fetch and execute remote INSTALL.md -curl -s https://raw.githubusercontent.com/mah0x211/claude-lua-devkit/master/INSTALL.md > .tmp/remote-install.md 2>/dev/null -``` - -IF fetch successful: -- Say: "✓ Downloaded latest installation procedure" -- Say: "Following remote installation steps for update..." -- Execute the installation steps from the remote INSTALL.md -- The remote installer will detect existing installation and handle update - -IF fetch failed: -Say: "Failed to download remote installer. Please check your internet connection or try manual update." - -### Show Help ("help") - -Display usage information: -``` -claude-lua-devkit Version Management - -Usage: /commands/ldk/version [command] - -Commands: - (no args) Show current version information - current Show current version information - list List available remote versions - ls List available remote versions (alias) - update Update to latest version - update Update to specific version (e.g., v1.0.0) - help Show this help message - -Examples: - /commands/ldk/version - /commands/ldk/version list - /commands/ldk/version update - /commands/ldk/version update v1.0.0 - -The update command uses the remote INSTALL.md for safe upgrading. -``` - -## Step 3: Completion Message - -After successful command execution: -- IF showing version: No additional message needed -- IF listing versions: Say "Use '/commands/ldk/version update [version]' to update" -- IF updating: Say "Update complete! Version information updated." -- IF help: No additional message needed \ No newline at end of file diff --git a/.claude/memories/contributing-sync.md b/.claude/memories/contributing-sync.md deleted file mode 100644 index 0d9a51c..0000000 --- a/.claude/memories/contributing-sync.md +++ /dev/null @@ -1,91 +0,0 @@ -# CONTRIBUTING.md and CI Workflow Synchronization - -Critical memory: CONTRIBUTING.md and GitHub Actions workflows must remain synchronized. - -## Sync Requirements - -### Files to Keep in Sync -- `/CONTRIBUTING.md` - Developer documentation -- `/.github/workflows/test.yml` - CI pipeline -- `/Makefile` - Build system (install target) -- `/sentry-*.rockspec` - LuaRocks dependencies - -### Critical Sync Points - -#### System Dependencies -**Ubuntu/Debian:** -```bash -sudo apt-get update -sudo apt-get install -y libssl-dev -``` - -**macOS:** -```bash -brew install openssl -export OPENSSL_DIR=$(brew --prefix openssl) -``` - -**Windows:** -- SSL support built-in -- MSVC build tools (ilammy/msvc-dev-cmd@v1) - setup before Lua installation - -#### Lua Environment -- **Versions**: Lua 5.1, 5.2, 5.3, 5.4, LuaJIT (all supported) -- **Package Manager**: LuaRocks - -#### LuaRocks Dependencies (must match Makefile install target) -1. `busted` - Testing framework -2. `tl` - Teal compiler -3. `lua-cjson` - JSON library -4. `luasocket` - HTTP client -5. `luasec` - SSL/TLS support -6. `luacov` - Code coverage -7. `luacov-reporter-lcov` - Coverage reporting -8. `tealdoc` - Documentation (install-all only) - -#### Build Commands -- `make install` - Install dependencies -- `make build` - Build project -- `make test` - Run tests -- `make coverage-report` - Tests with coverage (Linux/macOS) -- `make clean` - Clean build artifacts - -#### Platform Matrix -- **Ubuntu Latest** - Full testing + coverage + Codecov (Lua 5.4 only) -- **macOS Latest** - Full testing + coverage -- **Windows Latest** - Basic testing only (no coverage) - -#### Lua Version Matrix -- **Lua 5.1, 5.2, 5.3, 5.4** - Standard Lua versions -- **LuaJIT** - Just-in-Time compiler - -## Update Process - -When changing build requirements: - -1. **Update GitHub Actions workflow** (`.github/workflows/test.yml`) -2. **Update CONTRIBUTING.md** to match -3. **Update Makefile** if dependencies change -4. **Update rockspec** if runtime dependencies change -5. **Test locally** on your platform -6. **Verify CI passes** on all platforms - -## Last Sync Verification - -Always verify these elements match across files: -- [ ] System dependency installation commands -- [ ] Lua version requirement -- [ ] LuaRocks package list and versions -- [ ] Make targets used in CI -- [ ] Platform-specific considerations -- [ ] Build and test command sequences - -## Usage in Development - -When updating project requirements: -1. Check this memory file first -2. Update all sync points simultaneously -3. Test the full workflow locally -4. Verify CI pipeline succeeds - -This prevents contributor confusion and CI failures from documentation drift. \ No newline at end of file diff --git a/.claude/memories/github-actions-security.md b/.claude/memories/github-actions-security.md deleted file mode 100644 index 6541b59..0000000 --- a/.claude/memories/github-actions-security.md +++ /dev/null @@ -1,86 +0,0 @@ -# GitHub Actions Security Best Practices - -Critical memory: Always use commit SHAs instead of version tags for GitHub Actions. - -## Security Requirement - -**ALWAYS use commit SHAs instead of version tags** for all GitHub Actions in workflows. - -### Format -```yaml -# Correct - use commit SHA with version comment -- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - -# Incorrect - never use version tags directly -- uses: actions/checkout@v4 -``` - -## Security Benefits - -1. **Immutable references**: Commit SHAs cannot be changed or moved -2. **Supply chain security**: Prevents tag hijacking and malicious updates -3. **Reproducible builds**: Exact same action code always used -4. **Audit trail**: Clear version tracking with proper documentation - -## Finding Commit SHAs - -For any GitHub Action version, find the commit SHA by: - -1. Navigate to the action's GitHub repository -2. Go to Releases page -3. Click on the specific version tag (e.g., v4, v1.2.3) -4. Copy the commit SHA from the release page -5. Use full SHA, not abbreviated version - -## Comment Requirements - -**ALWAYS include version comment** after the commit SHA: - -```yaml -- uses: owner/action@ # -``` - -Examples: -```yaml -- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 -- uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 -``` - -## Maintenance Process - -When updating GitHub Actions: - -1. **Check for new releases** on action's GitHub repository -2. **Find new commit SHA** from release page -3. **Update workflow file** with new SHA -4. **Update comment** with correct version number -5. **Test thoroughly** before merging - -## Current Sentry Lua Project SHAs - -Reference SHAs used in this project (update when versions change): - -- `actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4` -- `actions/create-github-app-token@0f859bf9e69e887678d5bbfbee594437cb440ffe # v2.1.0` -- `ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1` -- `leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e # v10` -- `leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 # v4` -- `actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4` -- `codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4` -- `codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1` - -### Internal Sentry Actions (may use version tags) - -- `getsentry/action-prepare-release@v1` - Internal Sentry action, may not follow SHA pinning - -## Validation - -Before merging workflow changes: - -- [ ] All actions use full commit SHAs (not abbreviated) -- [ ] All SHAs have correct version comments -- [ ] No version tags (like @v4) remain in workflow -- [ ] Comments match actual release versions -- [ ] Workflow passes CI tests - -This prevents supply chain attacks and ensures reproducible, secure CI/CD pipelines. \ No newline at end of file diff --git a/.claude/memories/ldk-c-coverage.md b/.claude/memories/ldk-c-coverage.md deleted file mode 100644 index 4ed393d..0000000 --- a/.claude/memories/ldk-c-coverage.md +++ /dev/null @@ -1,112 +0,0 @@ -# C Module Coverage Procedures - -**This document describes coverage procedures specifically for C extension modules using lcov/gcov tools.** -For pure Lua module coverage procedures, see @.claude/memories/ldk-coverage.md - -## Build with Coverage Support -- Use project-specific environment variable to enable --coverage flag during build -- Example: `EXAMPLE_COVERAGE=1 luarocks make` -- This enables compiler coverage instrumentation for C extensions - -## Test Execution -- Run tests normally after coverage-enabled build: `testcase test/module_test.lua` -- DO NOT add `require('luacov')` to test files for C modules - -## Coverage Report Generation -Create a coverage generation script (e.g., `covgen.sh`) with content similar to: - -```bash -#!/bin/bash -# Generate coverage report for C modules - -set -e - -# Check if src directory exists -if [ ! -d "./src" ]; then - echo "Warning: src directory not found. Skipping coverage generation." - exit 0 -fi - -# Check if .gcno files exist in src directory -if ! find ./src -name "*.gcno" | grep -q .; then - echo "Warning: No .gcno files found in src directory. Skipping coverage generation." - echo "Note: To generate coverage data, compile and link with --coverage flag" - echo " Compile: gcc -c --coverage module.c" - echo " Link: gcc --coverage -o module.so module.o -shared" - exit 0 -fi - -echo "Generating coverage report..." - -# Clean previous coverage data -if [ -d "coverage" ]; then - echo "Removing previous coverage report..." - rm -rf coverage -fi - -# Get absolute path of src directory (resolves symlinks) -SRC_ABS_PATH=$(cd ./src && pwd) - -# Generate coverage data -lcov --capture --directory ./src --output-file coverage.info --ignore-errors gcov,source - -# Check if coverage data was generated successfully -if [ ! -f "coverage.info" ]; then - echo "Error: Failed to generate coverage.info" - exit 1 -fi - -# Extract only files from project's src directory -lcov --extract coverage.info "${SRC_ABS_PATH}/*" --output-file coverage.info --ignore-errors source - -# Generate HTML report -genhtml coverage.info --output-directory coverage --title "Project Coverage Report" - -# Check if HTML report was generated successfully -if [ ! -d "coverage" ] || [ ! -f "coverage/index.html" ]; then - echo "Error: Failed to generate HTML coverage report" - exit 1 -fi - -echo "Coverage report generated successfully!" -echo "HTML report: $(pwd)/coverage/index.html" -echo "" -echo "Coverage summary:" -lcov --summary coverage.info -``` - -## Usage -- Make script executable: `chmod +x covgen.sh` -- Generate report: `./covgen.sh` -- View HTML report in `coverage/index.html` - -## Platform-Specific Troubleshooting - -### macOS Issues -**Command not found**: Use `brew install lcov` if Homebrew installation failed -**Permission denied**: Check Xcode Command Line Tools installation with `xcode-select --install` -**Coverage data missing**: Verify GCC usage instead of Clang by setting `CC=gcc` in environment - -### Linux Issues -**Ubuntu/Debian**: Install `build-essential` package if GCC missing -**CentOS/RHEL**: Use `yum groupinstall "Development Tools"` for complete toolchain -**Generic Linux**: Ensure `--coverage` flag support with `gcc --help | grep coverage` - -### Build Integration Problems -**No .gcno files**: Verify @Makefile uses `--coverage` flag in both CFLAGS and LDFLAGS -**Partial coverage**: Check all C source files compile with coverage enabled -**Clean builds**: Remove `.gcda` files between runs to avoid stale data - -## Notes -- Requires lcov/gcov tools to be installed -- Coverage data is collected during test execution -- Clean build artifacts between coverage runs if needed -- **Coverage Target**: 95%+ (aim for 100% when practical) - -## Related Files -- @.claude/memories/ldk-coverage.md - Coverage procedures for pure Lua modules -- @.claude/memories/ldk-technology-stack.md - Installation instructions for lcov/gcov tools -- @.claude/memories/ldk-commands.md - Common development commands including build -- @.claude/memories/ldk-task-checklist.md - Complete development workflow including coverage -- @.claude/memories/ldk-test-guidelines.md - Guidelines for writing testable C extension code -- @.claude/memories/ldk-project-structure.md - Directory structure for C extensions and tests \ No newline at end of file diff --git a/.claude/memories/ldk-code-style.md b/.claude/memories/ldk-code-style.md deleted file mode 100644 index 692b5a6..0000000 --- a/.claude/memories/ldk-code-style.md +++ /dev/null @@ -1,169 +0,0 @@ -# Code Style Conventions - -**This document defines coding standards and best practices for Lua projects, including performance optimizations and function organization rules.** - -## Built-in Function Usage Conventions - -For performance and readability improvements, follow these conventions: - -### File-scope Local Variable Assignment - -Assign frequently used functions to file-scope local variables for performance and reliability: - -**What to localize:** -- Global functions: `local type = type`, `local pairs = pairs` -- Table methods: `local insert = table.insert`, `local floor = math.floor` -- Module functions: `local encode = require('json').encode` - -**Exceptions (keep global):** -- `assert` and `error` functions should remain global -- These may be overridden by application frameworks for error capture/handling -- Example: `assert(condition, "error message")` - use directly without localization - -**Why localize:** -1. **Performance**: Eliminates global table lookups -2. **Reliability**: Protects against runtime modifications -3. **Clarity**: Makes dependencies explicit - -### Avoid Object Method Syntax - -Use function form instead of method syntax to prevent issues with modified metatables: - -```lua --- Bad: Method syntax can fail if metatable is modified -local upper = str:upper() - --- Good: Function form is reliable -local upper = string.upper -local result = upper(str) -``` - -## Table Tail Append Optimization - -For performance improvement, use direct index operations for table tail appends: - -### Tail Append Optimization - -```lua --- Bad (function call overhead) -table.insert(tbl, value) -local insert = table.insert -insert(tbl, value) - --- Good (direct index operation, fastest) -tbl[#tbl + 1] = value -``` - -### Rationale - -1. **Performance**: Eliminates function call overhead -2. **Memory efficiency**: Direct operation minimizes memory access -3. **Clarity**: Intent of tail append is clear -4. **Consistency**: Uniform across entire project - -### Notes - -- Use `table.insert` for mid-table insertion or when nils are present -- Apply this convention only for tail append operations - -## Function Definition Order Rule - -**Critical Rule**: Functions must be defined in **dependency order from deepest to shallowest**. - -This follows the same constraint as C static functions without forward declarations: called functions must be defined before (above) their callers. - -### Implementation Strategy - -1. **Analyze the main function's call sequence** -2. **Place functions from top to bottom in order of dependency depth** -3. **Deepest dependencies first, main function last** - -### Example Pattern - -```lua --- Deepest dependency (used by intermediate functions) -local function helper_func() - return some_value -end - --- Intermediate dependency (used by main function) -local function process_data() - return helper_func() -- uses helper_func -end - --- Main function (uses all above functions) -local function main() - return process_data() -- uses process_data -end -``` - -### Real-world Application - -For a function that calls A, then B, then C (conditionally), arrange as: - -```lua --- Deepest/most distant calls first -local function C() ... end - --- Mid-level dependencies -local function B() ... end - --- Direct dependencies of main -local function A() ... end - --- Main function last -local function main() - A() -- first call - B() -- second call - if condition then - C() -- conditional call - end -end -``` - -**Key Principle**: Read the main function's call sequence, then arrange dependencies in reverse dependency order (deepest first, main last). - -### Implementation Decision Tree - -**For simple linear calls**: Place called functions immediately above caller -**For conditional calls**: Place deepest conditional functions first -**For shared utilities**: Place at top of file before any callers -**For recursive functions**: Define before any external callers - -### Performance Impact - -**Local variable caching** provides 15-25% performance improvement in tight loops. **Direct table indexing** (`tbl[#tbl + 1] = value`) outperforms `table.insert()` by 10-20% for tail appends. **Function definition order** enables better Lua compiler optimization. - -### Application Scope - -This convention applies to: -- All Lua files under lua/ -- All C files under src/ (similar optimization with corresponding C functions) -- Test files under test/ - -### Benefits - -1. **Performance**: Reduces table lookup overhead -2. **Readability**: Functions in use are immediately visible -3. **Debug efficiency**: Function references are clear -4. **Consistency**: Unified style across entire project -5. **Natural flow**: Matches C static function constraints - -## Common Optimization Patterns - -### Memory Efficiency -- **Pre-allocate tables** with known size using `table.new(narray, nhash)` when available -- **Reuse tables** instead of creating new ones in loops -- **String concatenation**: Use `table.concat()` for multiple strings, direct concatenation for 2-3 strings - -### Error Handling Optimization -- **Fast path first**: Place common success cases before error checks -- **Early returns**: Use guard clauses to reduce nesting depth -- **Error context**: Include sufficient context in error messages for debugging without verbose traces - -## Related Files -- @.claude/memories/ldk-commands.md - Commands for linting and formatting (luacheck, lua-format) -- @.claude/memories/ldk-test-guidelines.md - Code style in test files and test organization -- @.claude/memories/ldk-task-checklist.md - Style checking as part of development workflow -- @.claude/memories/ldk-technology-stack.md - Installation and setup for code quality tools -- @.claude/memories/ldk-project-structure.md - File organization and naming conventions diff --git a/.claude/memories/ldk-commands.md b/.claude/memories/ldk-commands.md deleted file mode 100644 index 6f5e685..0000000 --- a/.claude/memories/ldk-commands.md +++ /dev/null @@ -1,55 +0,0 @@ -# Lua Development Commands - -**This document lists common commands used during Lua project development, including build, test, and code quality tools.** -For detailed testing guidelines, see @.claude/memories/ldk-test-guidelines.md -For coverage procedures, see @.claude/memories/ldk-coverage.md (Lua) or @.claude/memories/ldk-c-coverage.md (C extensions). - -## Installation and Build -- `luarocks make` - Install the project (run this before testing) -- `make` - Build C extensions -- `make clean` - Clean build artifacts - -## Testing - -### testcase framework -- `testcase test/` - Run all tests (executes all `**/*_test.lua` files in directory) -- `testcase test/_test.lua` - Run specific test file -- `testcase test//` - Run tests in specific subdirectory - -### busted framework -- `busted` - Run all specs in spec/ directory -- `busted spec/_spec.lua` - Run specific spec file -- `busted spec//` - Run specs in specific subdirectory - -## Code Quality -- `luacheck .` - Run linting on Lua files -- `lua-format -i ` - Format a Lua file - -## Development Best Practices -- Always run `luarocks make` before testing -- Choose either `testcase` (test/) or `busted` (spec/) framework for consistency -- Coverage target: 95%+ (aim for 100% when practical) -- Code comments should be in English -- Follow project's formatting rules (4 spaces, single quotes) - -## Quick Reference -```bash -# Complete development cycle -luarocks make # Install/build project -testcase test/ # OR busted # Run all tests -luacheck . # Check code style -lua-format -i src/myfile.lua # Format specific file - -# Coverage workflows -require('luacov') # Add to Lua test files -EXAMPLE_COVERAGE=1 luarocks make # C extension coverage build -``` - -## Related Files -- @.claude/memories/ldk-technology-stack.md - Installation instructions for all development tools -- @.claude/memories/ldk-code-style.md - Detailed coding standards and style rules -- @.claude/memories/ldk-test-guidelines.md - Guidelines for writing and organizing tests -- @.claude/memories/ldk-task-checklist.md - Complete development workflow checklist -- @.claude/memories/ldk-coverage.md - Lua module coverage procedures -- @.claude/memories/ldk-c-coverage.md - C extension coverage procedures -- @.claude/memories/ldk-project-structure.md - Project organization and file structure \ No newline at end of file diff --git a/.claude/memories/ldk-coverage.md b/.claude/memories/ldk-coverage.md deleted file mode 100644 index 89d193d..0000000 --- a/.claude/memories/ldk-coverage.md +++ /dev/null @@ -1,112 +0,0 @@ -# Lua Module Coverage Procedures - -**This document describes coverage procedures for pure Lua modules using luacov.** -For C extension module coverage procedures, see @.claude/memories/ldk-c-coverage.md - -## Setup and Configuration -- Add `require('luacov')` at the top of test files -- Configure @.luacov file with module names and paths -- Set `deletestats = true` (can be changed to false to keep stats) - -### Example @.luacov Configuration -Create a @.luacov file in your project root: -```lua --- @.luacov configuration file -return { - -- Enable statistics collection - statsfile = "luacov.stats.out", - - -- Coverage report output file - reportfile = "luacov.report.out", - - -- Delete previous stats before new run (recommended for clean results) - deletestats = true, - - -- Modules to include in coverage (adjust paths to match your project) - include = { - "^lua/", -- Include all modules in lua/ directory - "^src/", -- Include modules in src/ directory - "mymodule", -- Include specific module - }, - - -- Modules to exclude from coverage - exclude = { - "test/", -- Exclude testcase files - "spec/", -- Exclude busted spec files - "bench/", -- Exclude benchmark files - "luacov$", -- Exclude luacov itself - "luarocks", -- Exclude luarocks modules - }, - - -- Coverage target (lines must be hit to count as covered) - -- Target: 95%+ coverage (aim for 100% when practical) -} -``` - -## Execution -- Run `luarocks make` to install/build project -- Execute tests: `testcase test/module_test.lua` -- Coverage report is automatically generated in `luacov.report.out` - -## Verification -- Review coverage report for completeness -- Identify untested code paths -- Add tests for uncovered areas to achieve target coverage (95%+) - -### Reading Coverage Reports -The `luacov.report.out` file shows: -- **Total Coverage**: Overall percentage across all included files -- **File Coverage**: Per-file line coverage statistics -- **Line Details**: Specific lines that were/weren't executed -- **Hit Count**: Number of times each line was executed - -Example report excerpt: - -``` -============================================================================== -mymodule.lua -============================================================================== - 1 : local function helper() - 2 : return "test" -- Hit 5 times - 3 : end - 4 : -*0 : local function unused() -- Never executed (0 hits) -*0 : return "unused" -*0 : end -``` - -Lines marked with `*0` indicate uncovered code that needs testing. - -## Configuration Patterns by Project Type - -### Simple Module Projects -**Single module**: Include pattern `"^mymodule$"`, exclude test directories (`"test/"` and `"spec/"`) -**Library projects**: Include `"^lua/"`, exclude test directories and external dependencies -**Mixed C/Lua**: Include Lua paths only, exclude C extensions (use separate C coverage) -**Framework choice**: Exclude `"test/"` for testcase or `"spec/"` for busted (or both for mixed projects) - -### Advanced Configuration Options -- **`includeuntestedfiles = true`**: Shows files with zero coverage (useful for new codebases) -- **`savestepsize = 100`**: Saves stats every 100 lines (useful for long-running tests) -- **`codefromstring = true`**: Includes coverage for dynamically loaded code - -## Troubleshooting Coverage Issues - -### Common Problems -**No coverage data**: Check `require('luacov')` appears before module loading -**Missing files**: Verify include/exclude patterns match your project structure -**Zero coverage**: Ensure test files are executing the target code paths -**Incomplete coverage**: Review exclude patterns - may be too broad - -### Performance Considerations -**Large projects**: Use specific include patterns to avoid tracking unnecessary files -**CI environments**: Set `deletestats = true` to ensure clean runs -**Development**: Set `deletestats = false` to accumulate coverage across test sessions - -## Related Files -- @.claude/memories/ldk-c-coverage.md - Coverage procedures for C extension modules -- @.claude/memories/ldk-technology-stack.md - Installation instructions for luacov and setup -- @.claude/memories/ldk-commands.md - Commands for running tests and coverage -- @.claude/memories/ldk-task-checklist.md - Complete development workflow including coverage -- @.claude/memories/ldk-test-guidelines.md - Writing tests that generate good coverage -- @.claude/memories/ldk-project-structure.md - Directory structure for modules and tests diff --git a/.claude/memories/ldk-index.md b/.claude/memories/ldk-index.md deleted file mode 100644 index 0f6856c..0000000 --- a/.claude/memories/ldk-index.md +++ /dev/null @@ -1,36 +0,0 @@ -# Lua Development Kit Memory Index - -Central reference point for all LDK memory files. - -## Core Development Memories - -@.claude/memories/lua-c-api.md - Lua C API development: luaopen_* functions, stack operations, userdata creation, and proper error handling with lua_error() -@.claude/memories/lua-module-patterns.md - Lua module development patterns: return table exports, require() usage, local vs global scope, and module initialization -@.claude/memories/luarocks-integration.md - LuaRocks build system: rockspec file structure, build dependencies, installation paths, and version management -@.claude/memories/makefile-patterns.md - Makefile development for Lua C extensions: compiler flags, library linking, installation rules, and cross-platform compatibility - -## Build System Memories - -@.claude/memories/build-system-design.md - Advanced build system architecture: mixed Lua/C/C++ projects, module grouping, and dynamic target generation -@.claude/memories/cross-platform-building.md - Cross-platform build considerations: compiler detection, platform-specific flags, library extensions (.so/.dll), and installation paths -@.claude/memories/ldk-source-directives.md - Source file directive system: @cflags:, @ldflags:, @reflibs:, @cppflags:, @cxxflags: for automatic build configuration in C/C++ source files -@.claude/memories/ldk-static-libraries.md - Static library build system: lib/ directory structure, .a file generation, prefix grouping, mixed C/C++ libraries, and @reflibs: linking - -## Project Structure Memories - -@.claude/memories/project-layout.md - Standard Lua project structure: src/ for C code, lua/ for Lua modules, spec/ for tests, and proper file organization -@.claude/memories/file-organization.md - File organization best practices: module naming conventions, directory hierarchies, and separation of concerns - -## Testing and Quality - -@.claude/memories/testing-strategies.md - Testing strategies for Lua projects: busted framework, spec files, C extension testing, and continuous integration setup - -## Documentation and Contribution - -@.claude/memories/contributing-sync.md - CONTRIBUTING.md and CI workflow synchronization: system dependencies, build requirements, platform matrix, and sync verification checklist -@.claude/memories/github-actions-security.md - GitHub Actions security: always use commit SHAs instead of version tags, with proper version comments for maintenance - -## Usage - -This index provides targeted access to LDK memory files based on development context. -Reference in CLAUDE.md with: `@.claude/memories/ldk-index.md` diff --git a/.claude/memories/ldk-memory-conventions.md b/.claude/memories/ldk-memory-conventions.md deleted file mode 100644 index 5bde03b..0000000 --- a/.claude/memories/ldk-memory-conventions.md +++ /dev/null @@ -1,25 +0,0 @@ -# Memory File Conventions - -**This document defines conventions for creating and maintaining memory files used with Serena MCP or similar memory management systems.** - -## Language Convention -- All memory files must be written in English -- This ensures consistency and accessibility across the project -- Technical documentation, procedures, and architectural notes should use English - -## File Organization Principles -- **Separate concerns**: Each memory file should focus on a single topic or domain -- **Avoid mixing**: Don't combine unrelated concepts in the same file -- **Clear naming**: File names should clearly indicate their purpose and scope - -## Content Guidelines -- Use clear, concise language that is easy to understand -- Structure content logically with appropriate headings -- Include practical examples where helpful -- Keep information current and accurate - -## Maintenance -- Regularly review memory files for relevance and accuracy -- Remove or update obsolete information -- Split files that become too large or cover multiple topics -- Merge files that are too small or closely related diff --git a/.claude/memories/ldk-project-structure.md b/.claude/memories/ldk-project-structure.md deleted file mode 100644 index 38ba29d..0000000 --- a/.claude/memories/ldk-project-structure.md +++ /dev/null @@ -1,49 +0,0 @@ -# Lua Project Structure Guidelines - -**This document defines the standard directory structure and file organization for Lua projects.** -For test code guidelines, see @.claude/memories/ldk-test-guidelines.md - -## Directory Organization - -**Path Structure Principle**: All directories mirror the `require()` module path structure. For `require('foo.bar.baz')`, files are organized as `/foo/bar/baz.`. - -- **Lua modules**: `lua/**/*.lua` -- **C extensions**: `src/**/*.c` -- **Tests**: `test/**/*_test.lua` (testcase) OR `spec/**/*_spec.lua` (busted) (one-to-one correspondence with modules) -- **Benchmarks**: `bench/**/*_bench.lua` or `test/bench/**/*_bench.lua` (one-to-one correspondence) -- **Documentation**: `doc/` or `docs/` -- **Configuration**: Root level (@.luacheckrc, @.luacov, etc.) - -## Module to Test Mapping -- **One-to-one correspondence**: Each module has exactly one test file -- **Path mirroring**: Test file paths mirror the module require path - -### testcase framework -- `require('foo.bar.baz')` → `test/foo/bar/baz_test.lua` -- `require('utils.string')` → `test/utils/string_test.lua` -- `require('parser')` → `test/parser_test.lua` - -### busted framework -- `require('foo.bar.baz')` → `spec/foo/bar/baz_spec.lua` -- `require('utils.string')` → `spec/utils/string_spec.lua` -- `require('parser')` → `spec/parser_spec.lua` - -## File Naming Conventions -- **Test files**: `_test.lua` (testcase) OR `_spec.lua` (busted) -- **Benchmark files**: `_bench.lua` -- **Module files**: Use clear, descriptive names matching functionality - -## Project Root Files -- @rockspec - LuaRocks specification file -- @Makefile - Build configuration (for C extensions) -- @.luacheckrc - Linting configuration -- @.luacov - Coverage configuration -- @README.md - Project documentation - -## Related Files -- @.claude/memories/ldk-test-guidelines.md - Detailed guidelines for test organization and structure -- @.claude/memories/ldk-commands.md - Commands that work with this directory structure -- @.claude/memories/ldk-code-style.md - File naming and organization conventions -- @.claude/memories/ldk-coverage.md - Coverage procedures that follow this structure -- @.claude/memories/ldk-c-coverage.md - C extension structure and coverage -- @.claude/memories/ldk-memory-conventions.md - General file organization principles diff --git a/.claude/memories/ldk-source-directives.md b/.claude/memories/ldk-source-directives.md deleted file mode 100644 index 35efc62..0000000 --- a/.claude/memories/ldk-source-directives.md +++ /dev/null @@ -1,186 +0,0 @@ -# Source File Directives for Lua C Extensions - -Comprehensive guide to using comment directives in C/C++ source files for automatic build configuration. - -## Overview - -Source file directives allow you to specify compiler and linker flags directly in your C/C++ source files using specially formatted comments. The makemk.lua script automatically parses these directives and applies them during compilation and linking. - -## Directive Syntax - -All directives use the format: `//@directive: value` - -- Must be in comment blocks (single-line `//` or multi-line `/* */`) -- Case-insensitive directive names -- Must appear before any actual code (before first non-comment, non-preprocessor line) -- Each directive can only appear once per file - -## Available Directives - -### @cppflags: - Preprocessor Flags -Used for C/C++ preprocessor directives, include paths, and macro definitions. - -```c -//@cppflags: -DDEBUG -DVERSION=1.0 -Iinclude -I/usr/local/include - -#include -// rest of code... -``` - -**Common usage:** -- `-DMACRO_NAME` - Define preprocessor macros -- `-DMACRO=value` - Define macros with values -- `-Ipath` - Add include directories -- `-I/absolute/path` - Absolute include paths - -### @cflags: - C Compiler Flags -Specific flags for C compilation (*.c files). - -```c -//@cflags: -Wall -Werror -std=c11 -pedantic - -#include -int main() { return 0; } -``` - -**Common usage:** -- `-Wall` - Enable all warnings -- `-Werror` - Treat warnings as errors -- `-std=c11` - Set C standard version -- `-pedantic` - Strict standard compliance -- `-O2`, `-O3` - Optimization levels - -### @cxxflags: - C++ Compiler Flags -Specific flags for C++ compilation (*.cpp files). - -```cpp -//@cxxflags: -std=c++17 -Wall -Wextra - -#include -int main() { return 0; } -``` - -**Common usage:** -- `-std=c++11/14/17/20` - C++ standard version -- `-Wall -Wextra` - Extended warnings -- `-fno-exceptions` - Disable exceptions -- `-fno-rtti` - Disable RTTI - -### @ldflags: - Linker Flags -Flags passed to the linker for library linking and linker options. - -```c -//@ldflags: -lm -lpthread -L/usr/local/lib -lcurl - -#include -#include -// code using math and pthread functions... -``` - -**Common usage:** -- `-lname` - Link against library (e.g., `-lm` for math library) -- `-Lpath` - Add library search path -- `-Wl,option` - Pass options directly to linker -- `-framework name` - Link macOS frameworks - -### @reflibs: - Static Library References -References to static libraries built in the lib/ directory. - -```c -//@reflibs: string util/memory - -#include "string_utils.h" -#include "util/memory.h" -// code that uses functions from lib/string.a and lib/util/memory.a -``` - -**Usage:** -- Space-separated list of library names -- `string` → links against `lib/string.a` -- `util/memory` → links against `lib/util/memory.a` -- Automatically handles dependency ordering - -## Multi-Line Comments - -Directives work in both single-line and multi-line comments: - -```c -/* - * @cppflags: -DDEBUG -Iinclude - * @cflags: -Wall -Werror - * @ldflags: -lm -lpthread - * @reflibs: string util/memory - */ - -#include -// rest of code... -``` - -## Complex Example - -Real-world example combining multiple directives: - -```c -/* - * Network module with PostgreSQL and cURL dependencies - * @cppflags: -DUSE_POSTGRESQL -DUSE_CURL -DUSE_ZLIB - * @cflags: -Wall -Werror -std=c11 - * @ldflags: -lpq -lcurl -lz - * @reflibs: string util/memory - */ - -#include -#include -#include -#include "string_utils.h" -#include "util/memory.h" - -// Network module implementation... -``` - -This generates a module that: -- Defines macros for PostgreSQL, cURL, and zlib support -- Uses strict C11 compilation -- Links against PostgreSQL, cURL, and zlib system libraries -- Links against project's string and util/memory static libraries - -## Error Handling - -### Duplicate Directive Error -```c -//@cflags: -Wall -//@cflags: -Werror // ERROR: Duplicate directive -``` - -### Invalid Placement -```c -#include // Code started -//@cflags: -Wall // ERROR: Too late, must come before code -``` - -## Integration with Build System - -1. **makemk.lua** scans all .c/.cpp files in src/ directory -2. Parses directives from file headers -3. Groups files by prefix into modules -4. Generates appropriate build targets with merged flags -5. **Makefile** executes the generated build rules - -## Best Practices - -1. **Place directives at file top** - Before any includes or code -2. **Use consistent formatting** - One directive per line for readability -3. **Document complex flags** - Add comments explaining unusual flags -4. **Group related directives** - Keep related flags together -5. **Test incrementally** - Add one directive at a time when debugging - -## Troubleshooting - -### Common Issues -- **Directive not found**: Check spelling and placement before code -- **Compilation errors**: Verify flag syntax and library availability -- **Linking failures**: Ensure referenced libraries exist and are in library paths -- **@reflibs not working**: Verify static library exists in lib/ directory - -### Debug Output -Run `lua makemk.lua` directly to see directive parsing output and any errors. \ No newline at end of file diff --git a/.claude/memories/ldk-static-libraries.md b/.claude/memories/ldk-static-libraries.md deleted file mode 100644 index 64d18ca..0000000 --- a/.claude/memories/ldk-static-libraries.md +++ /dev/null @@ -1,258 +0,0 @@ -# Static Library Build System - -Comprehensive guide to building and using static libraries (.a files) in Lua C extension projects. - -## Overview - -The static library system allows you to build reusable C/C++ code into static libraries (.a files) that can be linked into your dynamic Lua modules. This enables code sharing, better organization, and modular development for complex projects. - -## Directory Structure - -``` -project/ -├── lib/ # Static libraries (NEW in v0.2.0) -│ ├── string.c # → lib/string.a -│ ├── string_helper.c # → lib/string.a (grouped by prefix) -│ ├── util/ -│ │ └── memory.c # → lib/util/memory.a -│ └── parser/ -│ ├── json.c # → lib/parser/json.a -│ └── json_validator.cpp # → lib/parser/json.a (mixed C/C++) -├── lua/ # Lua modules (MOVED from lib/) -│ ├── mypackage.lua # Main module -│ └── mypackage/ -│ └── helper.lua # Sub-module -└── src/ # Dynamic Lua modules (.so files) - ├── core.c # → core.so (can link to lib/*.a) - └── network.c # → network.so -``` - -## Build Process - -### 1. Static Library Generation -```bash -# makemk.lua scans lib/ directory -# Groups files by prefix: string.c + string_helper.c → string module -# Generates build targets: lib/string.a, lib/util/memory.a, lib/parser/json.a - -lua makemk.lua # Creates mk/modules.mk with static library targets -``` - -### 2. Static Library Compilation -```bash -# C files compiled to object files -gcc -c lib/string.c -o lib/string.o -gcc -c lib/string_helper.c -o lib/string_helper.o - -# C++ files compiled with appropriate compiler -g++ -c lib/parser/json_validator.cpp -o lib/parser/json_validator.o - -# Object files archived into static libraries -ar rcs lib/string.a lib/string.o lib/string_helper.o -ar rcs lib/parser/json.a lib/parser/json.o lib/parser/json_validator.o -``` - -### 3. Dynamic Module Linking -```bash -# Dynamic modules can reference static libraries via @reflibs: directive -# Example in src/network.c: -# //@reflibs: string util/memory - -# Linking resolves static library dependencies automatically -gcc -o src/network.so src/network.o lib/string.a lib/util/memory.a -bundle -``` - -## Prefix Grouping Rules - -Static libraries follow the same prefix grouping rules as dynamic modules: - -### Simple Grouping -``` -lib/ -├── string.c # Base name: "string" -├── string_helper.c # Prefix matches: "string_" → grouped with string.c -└── string_utils.c # Prefix matches: "string_" → grouped with string.c -``` -**Result**: Single `lib/string.a` containing all three object files. - -### Separate Libraries -``` -lib/ -├── parser.c # Base name: "parser" -├── lexer.c # Base name: "lexer" (no prefix match) -└── tokenizer.c # Base name: "tokenizer" (no prefix match) -``` -**Result**: Three separate libraries: `lib/parser.a`, `lib/lexer.a`, `lib/tokenizer.a`. - -### Nested Directories -``` -lib/ -├── util/ -│ ├── memory.c # → lib/util/memory.a -│ └── memory_pool.c # Grouped with memory.c → lib/util/memory.a -└── parser/ - ├── json.c # → lib/parser/json.a - └── xml.c # → lib/parser/xml.a (separate, no prefix match) -``` - -## Mixed C/C++ Static Libraries - -Static libraries can contain both C and C++ object files: - -``` -lib/parser/ -├── json.c # Compiled with gcc -└── json_validator.cpp # Compiled with g++, adds -lstdc++ to linker flags -``` - -**Generated makefile target**: -```makefile -lib_parser_json_LINKER = $(CXX) # Uses C++ linker -lib_parser_json_LDFLAGS = -lstdc++ # Adds C++ standard library -``` - -Any dynamic module linking to this static library will automatically use the C++ linker. - -## Using Static Libraries in Dynamic Modules - -### 1. Reference in Source File -Use the `@reflibs:` directive to specify static library dependencies: - -```c -// src/network.c -//@reflibs: string util/memory parser/json - -#include "string_utils.h" -#include "util/memory.h" -#include "parser/json.h" - -// Network module implementation using static library functions -``` - -### 2. Automatic Linking -The build system automatically: -- Links specified static libraries: `lib/string.a lib/util/memory.a lib/parser/json.a` -- Inherits static library linker flags (e.g., `-lstdc++` for C++ libraries) -- Ensures proper dependency ordering -- Uses appropriate linker (gcc vs g++) - -### 3. Generated Build Target -```makefile -src/network.so: src/network.o lib/string.a lib/util/memory.a lib/parser/json.a - $(CXX) -o $@ $^ $(LDFLAGS) $(PLATFORM_LDFLAGS) -lstdc++ -``` - -## Advanced Usage Patterns - -### Layered Dependencies -``` -lib/ -├── foundation/ -│ └── memory.c # Base utilities -├── collections/ -│ └── string.c # Uses foundation (via @reflibs: foundation/memory) -└── protocols/ - └── http.c # Uses both (via @reflibs: foundation/memory collections/string) -``` - -### Platform-Specific Libraries -```c -// lib/platform/posix.c -//@cppflags: -D_POSIX_C_SOURCE=200809L -//@ldflags: -lpthread - -// Platform-specific implementation -``` - -### Third-Party Integration -```c -// lib/crypto/hash.c -//@cppflags: -I/usr/local/include -//@ldflags: -L/usr/local/lib -lcrypto -lssl - -// OpenSSL wrapper functions -``` - -## Build Integration - -### Generated Makefile Targets -makemk.lua generates targets for each static library: - -```makefile -# lib/string static library -lib_string_SRC := lib/string.c lib/string_helper.c -lib_string_OBJS := $(lib_string_SRC:.c=.o) -lib_string_LINKER = $(CC) -lib_string_LDFLAGS = - -lib/string.a: $(lib_string_OBJS) - @mkdir -p $(@D) - $(AR) rcs $@ $^ -``` - -### Module Dependencies -Dynamic modules with `@reflibs:` dependencies get: - -```makefile -src_network_LDFLAGS = lib/string.a lib/util/memory.a -src/network.so: src/network.o lib/string.a lib/util/memory.a - $(CC) -o $@ $^ $(LDFLAGS) $(src_network_LDFLAGS) -``` - -## Best Practices - -### Organization -1. **Group related functionality** - Use prefix grouping for related functions -2. **Separate concerns** - Different subdirectories for different domains -3. **Minimize dependencies** - Keep static libraries focused and minimal - -### Header Files -1. **Create corresponding headers** - `lib/string.c` → `include/string.h` -2. **Use include guards** - Standard `#ifndef`/`#define`/`#endif` pattern -3. **Document interfaces** - Clear function documentation in headers - -### Testing -1. **Unit test static libraries** - Test libraries independently -2. **Integration testing** - Test dynamic modules using static libraries -3. **Coverage analysis** - Use coverage flags for static library code - -## Troubleshooting - -### Common Issues -- **Undefined symbols**: Static library not properly linked or missing functions -- **Multiple definitions**: Same symbol defined in multiple static libraries -- **Linker errors**: Wrong linker (gcc vs g++) for mixed C/C++ libraries -- **Missing libraries**: @reflibs references non-existent static library - -### Debug Commands -```bash -# List contents of static library -ar -t lib/string.a - -# Show symbol table -nm lib/string.a - -# Verbose build to see linking -make V=1 - -# Check generated makefile -cat mk/modules.mk -``` - -### Clean Rebuilds -```bash -# Clean all build artifacts including static libraries -make clean - -# Force regeneration of mk/modules.mk -rm -f mk/modules.mk -lua makemk.lua -``` - -## Migration from v0.1.1 - -In v0.1.1, the `lib/` directory was used for Lua modules. In v0.2.0+: -- **Lua modules moved**: `lib/` → `lua/` -- **Static libraries added**: New `lib/` directory for C/C++ static libraries -- **Update require paths**: Lua code may need `require('package.module')` updates -- **Update build scripts**: Any custom scripts referencing old `lib/` directory \ No newline at end of file diff --git a/.claude/memories/ldk-task-checklist.md b/.claude/memories/ldk-task-checklist.md deleted file mode 100644 index 4f1bc9f..0000000 --- a/.claude/memories/ldk-task-checklist.md +++ /dev/null @@ -1,57 +0,0 @@ -# Development Task Checklist - -**This document provides a comprehensive checklist for development tasks, ensuring quality and consistency across the project.** -For specific commands, see @.claude/memories/ldk-commands.md -For code style details, see @.claude/memories/ldk-code-style.md - -When completing a coding task, always: - -## 1. Before Starting -- Run build/installation commands to ensure environment is set up -- Read relevant documentation in project documentation directory -- Check existing code patterns in similar files - -## 2. During Development -- Follow project's code style conventions (indentation, naming) -- Write clear comments for complex logic -- Use established design patterns for new components -- Implement proper error handling patterns - -## 3. After Implementation -- Run linting tools to check for code quality issues -- Format code according to project standards -- Write comprehensive tests with appropriate coverage -- Run test suite to ensure all tests pass -- Achieve coverage target: 95%+ (aim for 100% when practical) - -## 4. Final Checks -- If native code was modified, ensure proper resource management -- Update relevant documentation if API changed -- Run coverage reports if project uses them -- Ensure no debug prints or temporary code remains -- Verify no restricted commands are used (if applicable) - -## Quality Standards -- Always verify tests pass before considering task complete -- Follow existing patterns and conventions in the codebase -- Maintain consistency with project architecture and design - -## Quick Reference Checklist -- [ ] `luarocks make` (setup environment) -- [ ] Read docs and check existing patterns -- [ ] Follow code style conventions -- [ ] Write comprehensive tests -- [ ] `testcase test/` OR `busted` (run tests) -- [ ] `luacheck .` (check code quality) -- [ ] Achieve 95%+ coverage target -- [ ] Update docs if API changed -- [ ] Clean up debug code - -## Related Files -- @.claude/memories/ldk-commands.md - Specific commands for each checklist step -- @.claude/memories/ldk-code-style.md - Style rules and formatting standards -- @.claude/memories/ldk-test-guidelines.md - Guidelines for comprehensive test writing -- @.claude/memories/ldk-coverage.md - Lua coverage procedures for checklist step -- @.claude/memories/ldk-c-coverage.md - C extension coverage procedures -- @.claude/memories/ldk-technology-stack.md - Tool setup and installation requirements -- @.claude/memories/ldk-project-structure.md - Project organization and file conventions \ No newline at end of file diff --git a/.claude/memories/ldk-technology-stack.md b/.claude/memories/ldk-technology-stack.md deleted file mode 100644 index f6a36b9..0000000 --- a/.claude/memories/ldk-technology-stack.md +++ /dev/null @@ -1,143 +0,0 @@ -# Technology Stack - -**This document outlines the core technologies and tools used in Lua projects.** -For development commands using these tools, see @.claude/memories/ldk-commands.md - -## Core Language and Runtime - -- **Language**: Lua (5.1.5+) with C extensions for performance-critical parts - - **Required Version**: Lua 5.1.5 (mandatory for LuaJIT compatibility) - - **Recommended**: Lua 5.3 or 5.4 for improved performance when LuaJIT not required - - **Platform Support**: Cross-platform (Linux, macOS, Windows) - - **LuaJIT Support**: All code must be compatible with LuaJIT's Lua 5.1.5 implementation - -## Build System and Package Management - -- **Package Manager**: luarocks - - **Prerequisite**: luarocks must be installed and functional - - **Version**: 3.0+ recommended for better dependency resolution - - **Usage**: Manages Lua packages and dependencies - -- **Build System**: @Makefile with luarocks integration - - **Purpose**: Compiles C extensions and manages build artifacts - - **Requirements**: GCC or compatible C compiler for C extensions - - **Integration**: `luarocks make` automatically invokes @Makefile for C components - -## Testing Framework - -Choose one testing framework for your project: - -### Option 1: lua-testcase -- **Installation**: `luarocks install testcase` -- **Directory**: `test/` with `*_test.lua` files -- **Usage**: `testcase test/` runs all tests recursively -- **Assertions**: Uses lua-assert for expressive test assertions -- **Reference**: For detailed usage, refer to https://github.com/mah0x211/lua-testcase - -### Option 2: busted -- **Installation**: `luarocks install busted` -- **Directory**: `spec/` with `*_spec.lua` files -- **Usage**: `busted` runs all specs in spec/ directory -- **Features**: BDD-style testing with describe/it syntax -- **Reference**: For detailed usage, refer to https://github.com/lunarmodules/busted - -## Code Quality Tools - -- **Linting**: luacheck - - **Installation**: `luarocks install luacheck` - - **Purpose**: Static analysis and style checking for Lua code - - **Configuration**: @.luacheckrc file in project root - - **Usage**: `luacheck .` for project-wide linting - -- **Formatting**: lua-format - - **Installation**: `luarocks install lua-format` or platform-specific package managers - - **Purpose**: Automatic code formatting for consistency - - **Standards**: 4 spaces indentation, single quotes preferred - - **Usage**: `lua-format -i ` for in-place formatting - -## Coverage Tools - -- **Lua Module Coverage**: luacov - - **Installation**: `luarocks install luacov` - - **Purpose**: Line coverage analysis for pure Lua code - - **Configuration**: @.luacov file for coverage settings - - **Output**: `luacov.report.out` with detailed coverage report - -- **C Extension Coverage**: lcov/gcov - - **Installation**: - - **Ubuntu/Debian**: `apt-get install lcov` - - **CentOS/RHEL**: `yum install lcov` or `dnf install lcov` - - **macOS**: `brew install lcov` - - **Purpose**: Line coverage analysis for C extension code - - **Requirements**: GCC with `--coverage` flag support - - **Output**: HTML coverage reports via `genhtml` - -## Development Dependencies - -- **C Compiler**: GCC 4.8+ or Clang 3.5+ - - **Purpose**: Required for building C extensions - - **Features**: Must support `--coverage` flag for coverage analysis - -- **Make**: GNU Make 3.8+ - - **Purpose**: Build automation for C extensions - - **Platform**: Available on most Unix-like systems - -## Development Tools Installation - -**Prerequisites**: Lua 5.1.5+ and luarocks must be installed via your preferred method (version managers, package managers, or source compilation). - -### Tool Installation via luarocks -```bash -# Choose one testing framework -luarocks install testcase # OR luarocks install busted - -# Essential development tools -luarocks install luacheck -luarocks install luacov -luarocks install lua-format - -# Platform-specific coverage tools -# Ubuntu/Debian: apt-get install lcov -# macOS: brew install lcov -# CentOS/RHEL: yum install lcov -``` - -## Version Compatibility - -- **Lua 5.1.5**: Required minimum - ensures LuaJIT compatibility -- **Lua 5.2**: Compatible but avoid 5.2-specific features for LuaJIT support -- **Lua 5.3**: Recommended for pure Lua projects - integer support, better performance -- **Lua 5.4**: Latest features but may not be LuaJIT compatible -- **LuaJIT**: Primary target runtime - significant performance benefits, industry standard - -## Integration Notes - -- All tools integrate through luarocks package manager -- C extensions require proper compiler toolchain setup -- Coverage tools require specific build flags for accurate reporting -- Testing framework expects specific file naming conventions (`*_test.lua` for testcase, `*_spec.lua` for busted) -- **LuaJIT Compatibility**: All code must run on both standard Lua 5.1.5 and LuaJIT without modification - -## Quick Reference -```bash -# Prerequisites: Lua 5.1.5+ and luarocks must be installed - -# Choose testing framework and install tools -luarocks install testcase # OR luarocks install busted -luarocks install luacheck -luarocks install luacov -luarocks install lua-format - -# Coverage tools (platform-specific) -# macOS: brew install lcov -# Ubuntu/Debian: apt-get install lcov -# CentOS/RHEL: yum install lcov -``` - -## Related Files -- @.claude/memories/ldk-commands.md - How to use these tools in daily development -- @.claude/memories/ldk-coverage.md - Using luacov for Lua module coverage -- @.claude/memories/ldk-c-coverage.md - Using lcov/gcov for C extension coverage -- @.claude/memories/ldk-task-checklist.md - Tool usage as part of development workflow -- @.claude/memories/ldk-code-style.md - Standards enforced by luacheck and lua-format -- @.claude/memories/ldk-test-guidelines.md - Using testcase framework effectively diff --git a/.claude/memories/ldk-test-guidelines.md b/.claude/memories/ldk-test-guidelines.md deleted file mode 100644 index 997495f..0000000 --- a/.claude/memories/ldk-test-guidelines.md +++ /dev/null @@ -1,177 +0,0 @@ -# Test Code Guidelines - -**This document provides comprehensive guidelines for writing and organizing test code in Lua projects.** -For project structure details, see @.claude/memories/ldk-project-structure.md -For coverage procedures, see @.claude/memories/ldk-coverage.md (Lua) or @.claude/memories/ldk-c-coverage.md (C extensions). - -## General Principles -- Write clean, maintainable test code that is easy to understand and modify -- Each test function should focus on a single aspect of functionality -- Use descriptive function names that clearly indicate what is being tested -- Keep tests concise while maintaining thorough coverage -- Follow the AAA pattern: Arrange, Act, Assert - -## Helper Functions and Shared Utilities -- Create reusable helper functions for common test setup patterns -- Place shared helpers in `test/helpers/` directory for cross-test reuse -- Use parameter-driven helpers instead of repetitive manual code -- Name helper functions clearly to indicate their purpose -- Example: `create_samples(name, times)` instead of `create_mock_samples_with_time_data()` -- Export helpers as modules: `return { create_samples = create_samples, generate_times = generate_times }` - -## Test Organization and Structure -- **One test file per module**: Each module should have exactly one corresponding test file -- **Mirror module structure**: Test file paths should mirror the module require path - - Module: `require('foo.bar.baz')` → Test: `test/foo/bar/baz_test.lua` - - Module: `require('utils.string')` → Test: `test/utils/string_test.lua` - - Module: `require('utils')` → Test: `test/utils_test.lua` -- Use consistent directory structure that matches the source code organization -- Use descriptive test file names with `_test.lua` suffix -- Organize test functions logically within files (basic cases first, edge cases last) -- Create subdirectories as needed to match the module hierarchy - -## Data Generation and Test Data -- Use helper functions for generating test data consistently -- Parameterize data generation to avoid hardcoded loops -- Example: `generate_times(base_ms, count, variance)` for time arrays -- Keep data generation functions simple and focused -- Use factories for complex object creation -- Prefer deterministic test data over random data when possible - -## Test Categories and Coverage -- **Unit Tests**: Test individual functions/modules in isolation -- **Integration Tests**: Test interaction between components -- **Edge Case Tests**: Test boundary conditions and error scenarios -- **Performance Tests**: Verify performance characteristics when needed -- Aim for high code coverage but focus on meaningful test scenarios -- Test both positive and negative cases (success and failure paths) - -## Test Independence and Isolation -- Each test should be independent and not rely on other tests -- Use setup/teardown functions when needed for test isolation -- Avoid shared mutable state between tests -- Mock external dependencies to ensure test reliability -- Use fresh data/objects for each test to prevent interference - -## Assertion Best Practices -- Use specific assertions that clearly indicate what is being tested -- Provide meaningful error messages in assertions -- Test one concept per assertion when possible -- Group related assertions logically -- Use custom assertion helpers for complex validations - -## Error Testing and Exception Handling -- Test both success and failure cases appropriately -- Use pcall for testing expected errors with clear assertions -- Example: `assert(not ok, 'Should error with single sample')` -- Verify error messages contain expected information -- Test error conditions thoroughly, not just happy paths - -## Test Naming Conventions -- **Test Files**: Follow the pattern `_test.lua` where module_path mirrors the require path - - `require('foo.bar.baz')` → `test/foo/bar/baz_test.lua` - - Always use `_test.lua` suffix for test files -- **Test Functions**: Use descriptive names that explain the scenario - - Format: `testcase.what_is_being_tested_and_expected_outcome()` - - Examples: `basic_two_samples()`, `single_sample_error()`, `zero_mean_edge_case()` -- Group related tests with consistent naming patterns -- Avoid generic names like `test1()` or `basic_test()` -- Test function names should be self-documenting - -## Code Quality and Maintainability -- Keep tests DRY (Don't Repeat Yourself) through good helper functions -- Make tests resilient to minor implementation changes -- Use table-driven tests for multiple similar scenarios where appropriate -- Regularly review and refactor test code for clarity and efficiency -- Write self-documenting code that reduces need for comments - -## Performance Considerations -- Minimize redundant operations in test setup -- Use appropriate sample sizes for test reliability vs speed -- Reuse test data where possible without compromising test isolation -- Consider test execution time in CI/CD pipelines -- Use test doubles (mocks/stubs) to improve test speed - -## Lua-Specific Guidelines -- Follow project's Lua formatting rules (4 spaces, single quotes) -- Use ipairs/pairs appropriately for table iteration -- Leverage Lua's table features for clean test data structures -- Use local variables appropriately to avoid global pollution -- Handle Lua-specific error patterns with pcall/xpcall - -## Test Documentation -- Write clear comments for complex test scenarios -- Document test intentions when behavior is not obvious -- Include references to requirements or specifications being tested -- Explain the reasoning behind specific test data choices -- Document known limitations or assumptions in tests - -## Continuous Improvement -- Monitor test execution metrics (speed, flakiness) -- Regular test code reviews focusing on clarity and coverage -- Remove or update obsolete tests when requirements change -- Refactor tests when they become difficult to maintain -- Share testing patterns and practices across the team - -## Quick Reference - -### Test File Structure - -#### testcase framework -```lua --- test/module_name_test.lua --- require('luacov') -- for coverage - -local testcase = require('testcase') -local module = require('module_name') - -function testcase.basic_functionality() - -- Basic positive test case -end -``` - -#### busted framework -```lua --- spec/module_name_spec.lua -require('luacov') -- for coverage - -local module = require('module_name') - -describe('module_name', function() - it('should handle basic functionality', function() - -- Basic positive test case - end) -end) -``` - -### Test Organization Patterns - -**Module testing**: One test file per module, mirroring require path structure -**Integration testing**: Separate `integration/` subdirectory for multi-module tests -**Performance testing**: Use `bench/` subdirectory with `_bench.lua` suffix -**Error testing**: Group error conditions at end of test file for clarity -**Framework choice**: Use either `test/` (testcase) or `spec/` (busted) consistently across project - -### Effective Test Structure Principles - -**Setup hierarchy**: Global setup → per-test setup → test execution → cleanup -**Data generation**: Create reusable factories for complex test objects -**Assertion strategy**: One primary assertion per test function, supporting assertions for context -**Test naming**: Descriptive names indicating input conditions and expected outcomes - -### Coverage Target -- **Target**: 95%+ (aim for 100% when practical) -- Focus on meaningful test scenarios over just line coverage - -### Framework Documentation References -- **testcase**: Refer to https://github.com/mah0x211/lua-testcase for advanced testing patterns -- **busted**: Refer to https://github.com/lunarmodules/busted for BDD-style testing and mocking features - -## Related Files -- @.claude/memories/ldk-commands.md - Commands for running tests (`testcase test/`) -- @.claude/memories/ldk-project-structure.md - Test file organization and naming conventions -- @.claude/memories/ldk-coverage.md - Coverage analysis for Lua test files -- @.claude/memories/ldk-c-coverage.md - Coverage for C extension testing -- @.claude/memories/ldk-task-checklist.md - Testing as part of development workflow -- @.claude/memories/ldk-code-style.md - Code style rules that apply to test files -- @.claude/memories/ldk-technology-stack.md - testcase framework installation and setup \ No newline at end of file diff --git a/.claude/memories/sentry-test-config.md b/.claude/memories/sentry-test-config.md deleted file mode 100644 index 88c28c4..0000000 --- a/.claude/memories/sentry-test-config.md +++ /dev/null @@ -1,20 +0,0 @@ -# Sentry Test Configuration - -## Default Test Organization and Project - -For all Sentry MCP testing and validation: -- **Organization**: `bruno-garcia` -- **Project**: `playground` - -This configuration is used for: -- End-to-end testing with real Sentry events -- MCP server validation of SDK functionality -- Integration testing across all platforms (Love2D, Roblox, etc.) - -## Usage - -When validating events reach Sentry, always use: -``` -organizationSlug: bruno-garcia -projectSlug: playground -``` \ No newline at end of file diff --git a/.claude/memories/teal-coding-standards.md b/.claude/memories/teal-coding-standards.md deleted file mode 100644 index 64ceea6..0000000 --- a/.claude/memories/teal-coding-standards.md +++ /dev/null @@ -1,27 +0,0 @@ -# Teal Coding Standards - -## File Type Guidelines - -**Use Teal (.tl) for:** -- All code under `src/` directory -- Core SDK implementation files -- Platform-specific modules -- Utility functions and libraries - -**Use Lua (.lua) for:** -- Examples under `examples/` directory -- Test files under `spec/` directory -- Build scripts and configuration files -- External integration scripts - -## Migration Tasks - -When .lua files exist under `src/`, they should be converted to .tl files for: -- Better type safety -- Consistent codebase standards -- Improved developer experience -- Automated type checking in CI - -## Implementation - -Always check `src/` directory for any .lua files that need conversion to .tl format. \ No newline at end of file diff --git a/.github/workflows/love2d.yml b/.github/workflows/love2d.yml index 16f8a55..33b4110 100644 --- a/.github/workflows/love2d.yml +++ b/.github/workflows/love2d.yml @@ -44,20 +44,15 @@ jobs: echo "OPENSSL_DIR=$(brew --prefix openssl)" >> $GITHUB_ENV - name: Install dependencies - run: make install + run: lua scripts/dev.lua install - - name: Install Love2D and run tests - run: make ci-love2d - - - name: Test example module loading - run: | - cd examples/love2d - LUA_PATH="../../build/?.lua;../../build/?/init.lua;;" lua test_modules.lua + - name: Run Love2D tests + run: lua scripts/dev.lua ci-love2d - name: Package Love2D example run: | cd examples/love2d - zip -r sentry-love2d-demo.love . -x "*.DS_Store" "test_modules.lua" + zip -r sentry-love2d-demo.love . -x "*.DS_Store" ls -la sentry-love2d-demo.love - name: Upload Love2D artifacts diff --git a/.github/workflows/test-rockspec.yml b/.github/workflows/test-rockspec.yml deleted file mode 100644 index d033ab1..0000000 --- a/.github/workflows/test-rockspec.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Test Rockspec - -# Test LuaRocks installation with 'luarocks install sentry/sentry' - -on: - push: - branches: - - main - - release/* - pull_request: - paths-ignore: - - "**.md" - -jobs: - test-rockspec: - name: Test 'luarocks install sentry/sentry' on clean system - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - - - name: Install Lua 5.4 - uses: leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e # v10 - with: - luaVersion: '5.4' - - - name: Install LuaRocks - uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 # v4 - - - name: Test rockspec installation - run: make test-rockspec-clean - - - name: Show LuaRocks path for debugging - if: failure() - run: | - eval "$(luarocks path --local)" - echo "LUA_PATH: $LUA_PATH" - echo "LUA_CPATH: $LUA_CPATH" - ls -la ~/.luarocks/lib/luarocks/rocks-5.*/sentry/ || true \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ffd91a..ac0f8d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,46 @@ env: LUAJIT_2_1_VERSION: "v2.1.0-beta3" jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + + - name: Setup Lua + uses: leafo/gh-actions-lua@35bcb06abec04ec87df82e08caa84d545348536e # v10 + with: + luaVersion: "5.4" + + - name: Setup LuaRocks + uses: leafo/gh-actions-luarocks@e65774a6386cb4f24e293dca7fc4ff89165b64c5 # v4 + + - name: Install dependencies + run: lua scripts/dev.lua install + + - name: Check formatting + run: lua scripts/dev.lua format-check + + - name: Format code with StyLua + if: failure() + run: lua scripts/dev.lua format + + - name: Commit formatted code + if: failure() + run: | + if [[ $(git status --porcelain) ]]; then + echo "Code was formatted. Committing changes." + git config --global user.name 'Sentry GitHub Bot' + git config --global user.email 'bot+github-bot@sentry.io' + git add . + git commit -m "Format code with StyLua [skip ci]" + git push + else + echo "No formatting changes needed." + fi + + - name: Run linter + run: lua scripts/dev.lua lint + test: strategy: fail-fast: false @@ -103,11 +143,10 @@ jobs: - name: Install dependencies shell: bash run: | - # For Ubuntu LuaJIT, use --local flag to avoid permission issues + # For Ubuntu LuaJIT, we need to handle luasec SSL configuration if [ "${{ matrix.os }}" = "ubuntu-latest" ] && [[ "${{ matrix.lua-version }}" == *"luajit"* ]]; then - echo "Installing dependencies locally for Ubuntu LuaJIT..." + # Install dependencies individually to handle SSL configuration luarocks install --local busted - luarocks install --local tl luarocks install --local lua-cjson luarocks install --local luasocket if [ -n "$OPENSSL_DIR" ]; then @@ -118,17 +157,15 @@ jobs: fi luarocks install --local luacov luarocks install --local luacov-reporter-lcov + luarocks install --local luacheck # Add local luarocks bin to PATH echo "$HOME/.luarocks/bin" >> $GITHUB_PATH else - make install + lua scripts/dev.lua install fi - - name: Build project - run: make build - - name: Run tests with coverage - run: make coverage-report + run: lua scripts/dev.lua coverage - name: Validate rockspec installation shell: bash @@ -136,9 +173,9 @@ jobs: # For Ubuntu LuaJIT, update the rockspec test to use local luarocks path if [ "${{ matrix.os }}" = "ubuntu-latest" ] && [[ "${{ matrix.lua-version }}" == *"luajit"* ]]; then eval "$(luarocks path --local)" - make test-rockspec + lua scripts/dev.lua test-rockspec else - make test-rockspec + lua scripts/dev.lua test-rockspec fi - name: Upload coverage artifacts @@ -146,16 +183,15 @@ jobs: with: name: coverage-${{ matrix.os }}-lua${{ matrix.lua-version }} path: | - coverage.info luacov.report.out - test-results.xml + luacov.stats.out retention-days: 7 - name: Upload coverage to Codecov (Ubuntu Lua 5.4 only) if: matrix.os == 'ubuntu-latest' && matrix.lua-version == '5.4' uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 with: - file: ./coverage.info + file: ./luacov.report.out flags: unittests name: codecov-${{ matrix.lua-version }} fail_ci_if_error: false @@ -182,7 +218,7 @@ jobs: cp README.md dist-temp/ || { echo "❌ README.md not found"; exit 1; } # Copy directories (recursively) - cp -r build dist-temp/ || { echo "❌ build directory not found. Run 'make build' first."; exit 1; } + cp -r src dist-temp/ || { echo "❌ src directory not found"; exit 1; } cp -r spec dist-temp/ || { echo "❌ spec directory not found"; exit 1; } cp -r examples dist-temp/ || { echo "❌ examples directory not found"; exit 1; } diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..5b335b3 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,50 @@ +-- default is: max +-- std = "lua51+lua52+lua53+lua54+luajit" + +exclude_files = { + ".luarocks/", + ".git/", + "examples/love2d/sentry/" -- symlink to 'src' +} + +files["spec/"] = { + -- Allow using busted globals in test files + globals = { + "describe", "it", "before_each", "after_each", "setup", "teardown", + "assert", "spy", "stub", "mock", "pending", "finally", + "insulate", "expose", + } +} + +local love = { "love", } +files["**/love2d*"] = { globals = love } + +local roblox = { "game", "DateTime", "task", "Instance", "getgenv", "shared", "Enum" } +files["**/roblox*"] = { globals = roblox } + +local defold = { "http", } +files["**/defold*"] = { globals = defold } + +local openresty = { "ngx", } +files["**/openresty*"] = { globals = openresty } + +local platform_bootstrapper = {} +local n = 0 +for _, t in ipairs{love, roblox, defold, openresty} do + table.move(t, 1, #t, n + 1, platform_bootstrapper) + n = n + #t +end + +files["src/sentry/platforms/init.lua"] = { globals = platform_bootstrapper } + +ignore = { + "212", -- unused argument (common in callbacks) + "213", -- unused loop variable (common in iterations) + "611", -- A line consists of nothing but whitespace. + "612", -- A line contains trailing whitespace. + "613", -- Trailing whitespace in a string. + "614", -- Trailing whitespace in a comment. +} + +max_line_length = 200 +max_cyclomatic_complexity = 20 \ No newline at end of file diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..be69b50 --- /dev/null +++ b/.styluaignore @@ -0,0 +1,6 @@ +# Ignore generated or third-party files +.luarocks/ +.git/ + +# symlink to 'src' +examples/love2d/sentry/ diff --git a/.teal-types.d.tl b/.teal-types.d.tl deleted file mode 100644 index 8168a90..0000000 --- a/.teal-types.d.tl +++ /dev/null @@ -1,17 +0,0 @@ -global record cjson - encode: function(table): string - decode: function(string): table -end - -global record socket_http - request: function(table): any, number -end - -global record ltn12 - source: record - string: function(string): any - end - sink: record - table: function({any}): any - end -end \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7215bab --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lua", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/examples/basic.lua", + }, + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9636c..9407ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ Sentry Hackweek 2025 - **Experimental** SDK -* Portable Lua SDK, written in Teal, Lua 5.1 compatible. +* Portable Lua SDK, Lua 5.1 compatible. * CI and tests on Standard Lua and LuaJIT on macOS and Linux * Sentry Features include: * Error reporting with source context and local variable values diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index ee00c38..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,34 +0,0 @@ -# claude-lua-devkit - -A comprehensive Lua development toolkit for Claude Code, providing memory files, build system templates, and development commands. - -## Project Purpose - -This project creates efficient Lua development-focused resources optimized for Claude Code usage, enabling developers to build production-ready Lua packages with C/C++ extensions. - -## Key Components - -### Memory Files -Specialized guidance for Lua development workflows and best practices. - -### Build System -Complete Makefile-based build system supporting: -- Pure Lua modules -- C/C++ extensions with automatic compiler detection -- Mixed-language projects -- LuaRocks integration -- Cross-platform compatibility - -### Development Commands -Claude Code integration commands for project setup and management. - -## Development Guidelines - -- **Token Efficiency**: Concise, targeted information over verbose documentation -- **Practical Focus**: Immediately actionable patterns and workflows -- **Cross-Platform**: Support macOS, Linux, and other Unix-like systems -- **Standards Compliance**: Follow Lua and LuaRocks conventions - -## Memory References - -@.claude/memories/ldk-index.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index faccd1a..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,279 +0,0 @@ -# Contributing to Sentry Lua SDK - -Thank you for your interest in contributing to the Sentry Lua SDK! This guide will help you get started with development. - -## Development Requirements - -### System Dependencies - -The following system dependencies are required depending on your platform: - -#### Ubuntu/Debian -```bash -sudo apt-get update -sudo apt-get install -y libssl-dev -``` - -#### macOS -```bash -brew install openssl -export OPENSSL_DIR=$(brew --prefix openssl) -``` - -#### Windows -SSL support is typically built-in with recent Lua installations. -MSVC build tools are automatically configured in CI for Lua compilation. - -### Lua Environment - -- **Lua 5.1, 5.2, 5.3, 5.4 or LuaJIT** (all versions supported) -- **LuaRocks** (package manager) - -### Development Dependencies - -Install all development dependencies with: - -```bash -make install -``` - -This will install: -- `busted` - Testing framework -- `tl` - Teal language compiler -- `lua-cjson` - JSON library -- `luasocket` - HTTP client -- `luasec` - SSL/TLS support -- `luacov` - Code coverage -- `luacov-reporter-lcov` - Coverage reporting - -### Documentation Dependencies (Optional) - -For building documentation: - -```bash -make install-all -``` - -This additionally installs: -- `tealdoc` - Documentation generator (requires LuaFileSystem) - -## Building the Project - -### Compile Teal to Lua - -```bash -make build -``` - -This automatically: -1. Creates all necessary build directories -2. Discovers all `.tl` files in `src/sentry/` -3. Compiles them to corresponding `.lua` files in `build/sentry/` - -### Clean Build Artifacts - -```bash -make clean -``` - -## Testing - -### Run Basic Tests - -```bash -make test -``` - -### Run Tests with Coverage - -```bash -make coverage-report -``` - -This generates: -- `luacov.report.out` - Detailed coverage report -- `coverage.info` - LCOV format for external tools - -### Test Coverage Requirements - -- New features should include comprehensive tests -- Maintain or improve existing code coverage -- All tests must pass on Linux, macOS, and Windows - -## Code Style and Quality - -### Teal Language - -This project is written in [Teal](https://github.com/teal-language/tl), a typed dialect of Lua. - -- Use proper type annotations -- Follow existing code patterns -- Maintain type safety across platform modules - -### Linting - -```bash -make lint # Strict linting (may fail on external modules) -make lint-soft # Permissive linting (warnings ignored) -``` - -### Code Organization - -``` -src/sentry/ -├── init.tl # Main SDK entry point -├── version.tl # Centralized version (auto-updated) -├── types.tl # Type definitions -├── core/ # Core SDK functionality -├── utils/ # Utility functions -└── platforms/ # Platform-specific implementations - ├── standard/ # Standard Lua/LuaJIT - ├── nginx/ # OpenResty/nginx - ├── roblox/ # Roblox game platform - ├── love2d/ # LÖVE 2D game engine - ├── defold/ # Defold game engine - ├── redis/ # Redis Lua scripting - └── test/ # Test transport -``` - -## Platform Support - -The SDK supports multiple Lua environments: - -- **Standard Lua** (5.1+) and LuaJIT -- **nginx/OpenResty** - Web server scripting -- **Roblox** - Game development platform -- **LÖVE 2D** - Game engine -- **Defold** - Game engine -- **Redis** - Lua scripting in Redis - -### Adding New Platforms - -1. Create `src/sentry/platforms/newplatform/` -2. Implement required transport modules -3. Add platform detection in `src/sentry/platform_loader.tl` -4. Add comprehensive tests - -## Documentation - -### Generate Documentation - -```bash -make docs -``` - -Generates HTML documentation in `docs/` directory. - -### Serve Documentation Locally - -```bash -make serve-docs -``` - -Starts local server at http://localhost:8000 - -## Version Management - -### Centralized Versioning - -The project uses a centralized version system: - -- **Single source**: `src/sentry/version.tl` -- **Auto-imported**: All transports reference this version -- **Auto-updated**: Bump scripts modify only this file - -### Version Bumping - -Version updates are handled by automated scripts: - -```bash -# Bump to new version (example: 0.0.2) -pwsh scripts/bump-version.ps1 0.0.2 -# or -./scripts/bump-version.sh dummy 0.0.2 -``` - -**Do not manually update version numbers in individual files.** - -## Continuous Integration - -### GitHub Actions Matrix - -The CI runs on: -- **Ubuntu Latest** (Linux) -- **macOS Latest** -- **Windows Latest** - -With Lua versions: -- **Lua 5.1, 5.2, 5.3, 5.4** -- **LuaJIT** - -### CI Requirements - -All contributions must: -1. **Build successfully** on all platforms -2. **Pass all tests** on all platforms -3. **Maintain code coverage** (Linux/macOS only) -4. **Follow code style** guidelines - -### Local CI Simulation - -Test your changes across platforms by running the same commands as CI: - -```bash -make install # Install dependencies -make build # Build project -make coverage-report # Run tests with coverage (Linux/macOS) -# or -make test # Run basic tests (Windows) -``` - -## Submitting Changes - -### Pull Request Process - -1. **Fork** the repository -2. **Create** a feature branch from `main` -3. **Make** your changes following this guide -4. **Test** locally on your platform -5. **Submit** a pull request - -### Pull Request Requirements - -- [ ] All CI checks pass (Linux, macOS, Windows) -- [ ] Tests cover new functionality -- [ ] Code follows existing patterns -- [ ] Documentation updated if needed -- [ ] Version not manually modified - -### Commit Messages - -Use clear, descriptive commit messages: - -``` -Add Redis Lua scripting transport support - -- Implement RedisTransport class -- Add Redis-specific configuration -- Include comprehensive tests -- Update platform loader -``` - -## Getting Help - -- **Issues**: Report bugs or request features via GitHub Issues -- **Discussions**: Join community discussions on GitHub -- **Documentation**: Refer to generated docs and existing code - -## Development Environment Sync - -> **⚠️ Important**: This CONTRIBUTING.md must stay synchronized with `.github/workflows/test.yml`. When updating build requirements or dependencies, update both files. - -### Current Sync Points -- System dependencies (SSL libraries) -- Lua version requirements -- LuaRocks dependencies -- Build and test commands -- Platform-specific considerations - -Last synced: [Current date] with GitHub Actions workflow v1.0 \ No newline at end of file diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md deleted file mode 100644 index f610fba..0000000 --- a/DISTRIBUTION.md +++ /dev/null @@ -1,146 +0,0 @@ -# LuaRocks Distribution Guide - -This document explains how the Sentry Lua SDK is distributed via LuaRocks with automatic Teal compilation. - -## Overview - -The Sentry Lua SDK is written in Teal and compiled to Lua. The rockspec uses a `command` build type to automatically compile Teal sources during installation: - -```lua --- sentry-0.0.4-1.rockspec -build = { - type = "command", - build_command = "make build", - install_command = "mkdir -p $(LUADIR)/sentry && cp -r build/sentry/* $(LUADIR)/sentry/", - -- ... -} -``` - -When users install via `luarocks install sentry/sentry`, LuaRocks: -1. Clones the Git repository at the specified tag -2. Runs `make build` to compile Teal sources to Lua -3. Installs the compiled Lua modules - -## Solution: Command-Based Build - -We use LuaRocks' `command` build type with automatic Teal compilation. - -### Local Development - -```bash -# Test rockspec installation -make test-rockspec - -# Build from source -make build - -# Install locally for development -luarocks make --local -``` - -### CI/CD Integration - -The GitHub Actions workflow in `.github/workflows/test.yml` automatically: -1. Installs Teal compiler and dependencies -2. Builds the Teal sources (`make build`) -3. Tests rockspec installation (`make test-rockspec`) - -### Release Process - -The release process: -1. Creates a git tag matching the version -2. The rockspec references this tag in the `source.tag` field -3. LuaRocks.org can install directly from the git repository - -## Distribution Methods - -### Method 1: LuaRocks.org (Recommended - macOS/Linux) -```bash -# Install from LuaRocks.org (requires Unix-like system for Teal compilation) -luarocks install sentry/sentry -``` -**Note:** Use `sentry/sentry` (not just `sentry`) as the plain `sentry` package is owned by someone else. - -### Method 2: Direct Download (Cross-platform) -For Windows or systems without make/compiler support: -1. Download the latest `sentry-lua-sdk-publish.zip` from GitHub Releases -2. Extract and add to your Lua path -3. No compilation required - contains pre-built Lua files - -### Method 3: Git Source (Development) -```bash -# Install from specific version (Unix-like systems only) -git clone https://github.com/getsentry/sentry-lua.git -cd sentry-lua -make build -luarocks make --local -``` - -## Compatibility - -- **Target**: Lua 5.1 compatibility (`tlconfig.lua`: `gen_target = "5.1"`, `gen_compat = "optional"`) -- **Tested**: Lua 5.1, 5.2, 5.3, 5.4, LuaJIT 2.0, LuaJIT 2.1 -- **Platform**: Pure Lua (no native code currently) - -## Build Process - -``` -Git Repository: -├── src/sentry/ (Teal sources) -│ ├── init.tl -│ ├── core/ -│ ├── platforms/ -│ └── utils/ -├── Makefile (Build system) -└── sentry-0.0.4-1.rockspec - -After `make build`: -├── build/sentry/ (Compiled Lua) -│ ├── init.lua -│ ├── core/ -│ ├── platforms/ -│ └── utils/ - -After `make publish`: -├── sentry-lua-sdk-publish.zip (Direct download package) - ├── build/sentry/ (Pre-compiled Lua files) - ├── examples/ (Usage examples) - ├── README.md - └── CHANGELOG.md -``` - -## Verification - -Test the rockspec installation works: - -```bash -# Test rockspec installation -make test-rockspec - -# Manual verification -luarocks make --local -eval "$(luarocks path --local)" -lua -e "local sentry = require('sentry'); print('✅ Success')" -``` - -## Advantages - -✅ **Automatic compilation** - Teal sources compiled during installation -✅ **Standard LuaRocks** - Uses official LuaRocks distribution methods -✅ **Git tag-based** - Proper versioning with git tags -✅ **Lua 5.1 compatible** - Works across all Lua versions -✅ **CI tested** - Installation process tested in CI - -## Maintenance - -- Rockspec tested automatically in CI across multiple Lua versions (5.1, 5.2, 5.3, 5.4, LuaJIT) -- Separate GitHub Actions workflow tests clean system installation -- `make test-rockspec` target for local testing with existing dependencies -- `make test-rockspec-clean` target for testing on clean systems (installs Teal automatically) -- Version bumping updates both rockspec version and git tag - -### Security - -- All GitHub Actions use pinned commit SHAs (not version tags) for security -- See `.claude/memories/github-actions-security.md` for SHA reference -- Actions are verified and updated following security best practices \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 782af7c..0000000 --- a/Makefile +++ /dev/null @@ -1,320 +0,0 @@ -.PHONY: build test test-coverage coverage-report test-love clean install install-teal docs install-love2d ci-love2d test-rockspec test-rockspec-clean publish roblox-all-in-one - -# Install Teal compiler (for fresh systems without Teal) -install-teal: - @if ! command -v tl > /dev/null 2>&1; then \ - echo "Installing Teal compiler..."; \ - luarocks install --local tl; \ - eval "$$(luarocks path --local)"; \ - else \ - echo "Teal compiler already available"; \ - fi - -# Build Teal files to Lua -build: install-teal - rm -rf build/ - find src/sentry -type d | sed 's|src/sentry|build/sentry|' | xargs mkdir -p - find src/sentry -name "*.tl" -type f | while read -r tl_file; do \ - lua_file=$$(echo "$$tl_file" | sed 's|src/sentry|build/sentry|' | sed 's|\.tl$$|.lua|'); \ - echo "Compiling $$tl_file -> $$lua_file"; \ - eval "$$(luarocks path --local)" && tl gen "$$tl_file" -o "$$lua_file" || exit 1; \ - done - -# Run unit tests -test: build - busted - -# Run unit tests with coverage -test-coverage: build - rm -f luacov.*.out test-results.xml - LUA_PATH="build/?.lua;build/?/init.lua;;" busted --coverage - luacov - @# Generate test results in JUnit XML format for codecov test analytics - LUA_PATH="build/?.lua;build/?/init.lua;;" busted --output=junit > test-results.xml - -# Run Love2D tests (requires Love2D to be installed) -test-love: build - @echo "Running Love2D unit tests with busted..." - LUA_PATH="build/?.lua;build/?/init.lua;;" busted spec/platforms/love2d/love2d_spec.lua --output=TAP - @echo "" - @echo "Running Love2D integration tests (headless)..." - @# Verify lua-https binary is available - @if [ ! -f examples/love2d/https.so ]; then \ - echo "❌ CRITICAL: lua-https binary not found at examples/love2d/https.so"; \ - echo "Love2D tests REQUIRE HTTPS support. Rebuild with:"; \ - echo "cd examples/love2d/lua-https && cmake -Bbuild -S. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=\$$PWD/install && cmake --build build --target install"; \ - exit 1; \ - fi - @# Copy binary to test directory - cp examples/love2d/https.so spec/platforms/love2d/ || { \ - echo "❌ Failed to copy https.so to test directory"; \ - exit 1; \ - } - @# Run Love2D integration tests with cross-platform timeout and virtual display - @if [ "$(shell uname)" = "Darwin" ]; then \ - echo "Running Love2D tests on macOS (no timeout command available)"; \ - cd spec/platforms/love2d && love . > test_output.log 2>&1 || true; \ - else \ - echo "Running Love2D tests with virtual display on Linux"; \ - cd spec/platforms/love2d && xvfb-run -a -s "-screen 0 1x1x24" timeout 30s love . > test_output.log 2>&1 || true; \ - fi - @# Validate test results - @if grep -q "All tests passed" spec/platforms/love2d/test_output.log; then \ - echo "✅ Love2D integration tests passed"; \ - cat spec/platforms/love2d/test_output.log; \ - else \ - echo "❌ Love2D integration tests failed or incomplete"; \ - cat spec/platforms/love2d/test_output.log; \ - rm -f spec/platforms/love2d/test_output.log spec/platforms/love2d/https.so; \ - exit 1; \ - fi - @# Clean up test artifacts - @rm -f spec/platforms/love2d/test_output.log spec/platforms/love2d/https.so - @echo "✅ All Love2D tests completed successfully" - -# Generate coverage report in LCOV format for codecov -coverage-report: test-coverage - @# Generate LCOV; do not swallow errors, and ensure non-empty output - @rm -f coverage.info - @echo "Generating LCOV with luacov.reporter.lcov ..." - @lua -e "require('luacov.reporter.lcov').report()" > coverage.info || true - @if [ ! -s coverage.info ]; then \ - echo "coverage.info is empty after luacov.reporter.lcov; trying 'luacov -r lcov'"; \ - luacov -r lcov > coverage.info 2>/dev/null || true; \ - fi - @if [ ! -s coverage.info ]; then \ - echo "LCOV generation still empty; falling back to raw luacov report"; \ - cp -f luacov.report.out coverage.info 2>/dev/null || true; \ - fi - @# Print quick stats to help debug in CI - @echo "File stats (lines words bytes):"; \ - ( [ -f coverage.info ] && echo "coverage.info:" && wc -l -w -c coverage.info ) || true; \ - ( [ -f luacov.report.out ] && echo "luacov.report.out:" && wc -l -w -c luacov.report.out ) || true; \ - ( [ -f luacov.stats.out ] && echo "luacov.stats.out:" && wc -l -w -c luacov.stats.out ) || true - @# Show SF lines BEFORE path remapping - @echo "SF lines BEFORE remap:"; \ - grep "^SF:" coverage.info || true - @# Map build/*.lua file paths (absolute or relative) to src/*.tl for Codecov - @# Handle both absolute and relative paths that contain build/sentry - @sed -i.bak 's|^SF:.*build/sentry/\(.*\)\.lua|SF:src/sentry/\1.tl|g' coverage.info - @# Show SF lines AFTER path remapping - @echo "SF lines AFTER remap:"; \ - grep "^SF:" coverage.info || true - @# Verify that mapped source files actually exist - @echo "Verifying coverage file paths..." - @grep "^SF:" coverage.info | sed 's/^SF://' | while read -r file; do \ - if [ ! -f "$$file" ]; then \ - echo "Warning: Source file $$file not found in repository"; \ - fi; \ - done - @# Clean up sed backup and finish - @rm -f coverage.info.bak - @echo "Coverage report generated at coverage.info" - -# Clean build artifacts -clean: - rm -rf build/ - rm -f luacov.*.out coverage.info test-results.xml - -# Install dependencies -install: - luarocks install busted - luarocks install tl - luarocks install lua-cjson - luarocks install luasocket - @if [ -n "$$OPENSSL_DIR" ]; then \ - echo "Installing luasec with OPENSSL_DIR=$$OPENSSL_DIR"; \ - luarocks install luasec OPENSSL_DIR=$$OPENSSL_DIR; \ - else \ - luarocks install luasec; \ - fi - luarocks install luacov - luarocks install luacov-reporter-lcov - -# Install all dependencies including docs tools -install-all: install - luarocks install tealdoc - -# Generate documentation -docs: build install-all - mkdir -p docs - tealdoc html -o docs --all src/sentry/*.tl src/sentry/core/*.tl src/sentry/utils/*.tl src/sentry/platforms/**/*.tl - -# Lint code (strict) -lint: - tl check src/sentry/init.tl - tl check src/sentry/core/context.tl - tl check src/sentry/core/transport.tl - tl check src/sentry/core/test_transport.tl - tl check src/sentry/core/file_io.tl - tl check src/sentry/core/file_transport.tl - tl check src/sentry/core/client.tl - tl check src/sentry/utils/*.tl - tl check src/sentry/platforms/**/*.tl - -# Lint code (permissive - ignore external module warnings) -lint-soft: - -tl check src/sentry/init.tl - -tl check src/sentry/core/context.tl - -tl check src/sentry/core/transport.tl - -tl check src/sentry/core/test_transport.tl - -tl check src/sentry/core/file_io.tl - -tl check src/sentry/core/file_transport.tl - -tl check src/sentry/core/client.tl - -tl check src/sentry/utils/*.tl - -tl check src/sentry/platforms/**/*.tl - @echo "Soft lint completed (warnings ignored)" - -# Docker tests -docker-test-redis: - docker-compose -f docker/redis/docker-compose.yml up --build --abort-on-container-exit - -docker-test-nginx: - docker-compose -f docker/nginx/docker-compose.yml up --build --abort-on-container-exit - -# Full test suite (excludes Love2D - requires Love2D installation) -test-all: test test-rockspec docker-test-redis docker-test-nginx - -# Install Love2D (platform-specific) -install-love2d: - @echo "Installing Love2D..." - @if [ "$$(uname)" = "Darwin" ]; then \ - echo "Installing Love2D on macOS..."; \ - if ! command -v love > /dev/null 2>&1; then \ - if command -v brew > /dev/null 2>&1; then \ - brew install --cask love; \ - else \ - echo "❌ Homebrew not found. Please install Homebrew first."; \ - exit 1; \ - fi; \ - else \ - echo "✅ Love2D already installed: $$(love --version)"; \ - fi; \ - elif [ "$$(uname)" = "Linux" ]; then \ - echo "Installing Love2D and virtual display on Linux..."; \ - if ! command -v love > /dev/null 2>&1; then \ - if command -v apt-get > /dev/null 2>&1; then \ - sudo add-apt-repository -y ppa:bartbes/love-stable; \ - sudo apt-get update; \ - sudo apt-get install -y love; \ - else \ - echo "❌ apt-get not found. Please install Love2D manually."; \ - exit 1; \ - fi; \ - else \ - echo "✅ Love2D already installed: $$(love --version)"; \ - fi; \ - else \ - echo "❌ Unsupported platform: $$(uname)"; \ - exit 1; \ - fi - @echo "✅ Love2D installation complete" - -# CI target for Love2D - install Love2D and run tests -ci-love2d: install-love2d build test-love - -# Full test suite including Love2D (requires Love2D to be installed) -test-all-with-love: test test-rockspec test-love docker-test-redis docker-test-nginx - -# Serve documentation locally -serve-docs: docs - @echo "Starting documentation server at http://localhost:8000" - @echo "Press Ctrl+C to stop" - python3 -m http.server 8000 --directory docs - -# Test rockspec by installing it in an isolated environment -test-rockspec: build - @echo "Testing rockspec installation and functionality..." - @rm -rf rockspec-test/ - @mkdir -p rockspec-test - @# Copy current rockspec and source files to test directory - @find . -maxdepth 1 -name "*.rockspec" -exec cp {} rockspec-test/ \; - @cp -r src rockspec-test/ - @cp tlconfig.lua rockspec-test/ 2>/dev/null || true - @# Create a minimal test application that only tests module loading - @echo 'local sentry = require("sentry")' > rockspec-test/test.lua - @echo 'print("✅ Sentry loaded successfully")' >> rockspec-test/test.lua - @echo '-- Test that we can access core functions without initializing' >> rockspec-test/test.lua - @echo 'if type(sentry.init) == "function" then' >> rockspec-test/test.lua - @echo ' print("✅ Sentry API available")' >> rockspec-test/test.lua - @echo 'end' >> rockspec-test/test.lua - @# Install the rockspec locally - @echo "Installing rockspec locally..." - @cd rockspec-test && find . -maxdepth 1 -name "*.rockspec" -exec luarocks make --local {} \; - @# Test basic functionality - @echo "Testing basic Sentry functionality..." - @cd rockspec-test && eval "$$(luarocks path --local)" && lua test.lua - @# Clean up - @echo "Cleaning up test environment..." - @rm -rf rockspec-test/ - @echo "✅ Rockspec validation completed successfully" - -# Test rockspec installation on a clean system (for CI) -test-rockspec-clean: - @echo "Testing rockspec installation on clean system..." - @rm -rf rockspec-clean-test/ - @mkdir -p rockspec-clean-test - @# Copy current rockspec to test directory - @find . -maxdepth 1 -name "*.rockspec" -exec cp {} rockspec-clean-test/ \; - @# Copy source files (needed for build) - @cp -r src rockspec-clean-test/ - @cp tlconfig.lua rockspec-clean-test/ 2>/dev/null || true - @# Create a minimal test application that only tests module loading - @echo 'local sentry = require("sentry")' > rockspec-clean-test/test.lua - @echo 'print("✅ Sentry loaded successfully")' >> rockspec-clean-test/test.lua - @echo '-- Test that we can access core functions without initializing' >> rockspec-clean-test/test.lua - @echo 'if type(sentry.init) == "function" then' >> rockspec-clean-test/test.lua - @echo ' print("✅ Sentry API available")' >> rockspec-clean-test/test.lua - @echo 'end' >> rockspec-clean-test/test.lua - @# Install build dependencies first - @echo "Installing build dependencies..." - @cd rockspec-clean-test && luarocks install --local tl - @echo "Installing sentry rockspec with all dependencies..." - @cd rockspec-clean-test && find . -maxdepth 1 -name "*.rockspec" -exec echo "Found rockspec: {}" \; - @cd rockspec-clean-test && find . -maxdepth 1 -name "*.rockspec" -exec luarocks make --local --verbose {} \; || { echo "❌ Rockspec installation failed"; luarocks list --local; exit 1; } - @echo "Verifying sentry module installation..." - @cd rockspec-clean-test && eval "$$(luarocks path --local)" && lua -e "require('sentry'); print('✅ Sentry module found')" || { echo "❌ Sentry module not found after installation"; exit 1; } - @# Test functionality - @echo "Testing Sentry functionality..." - @cd rockspec-clean-test && eval "$$(luarocks path --local)" && lua test.lua - @# Clean up - @echo "Cleaning up test environment..." - @rm -rf rockspec-clean-test/ - @echo "✅ Clean system rockspec validation completed successfully" - -# Create publish package for direct download (Windows/cross-platform) -# Contains pre-compiled Lua files, no LuaRocks or compilation required -publish: build - @echo "Creating publish package for direct download (Windows/cross-platform)..." - @echo "This package contains pre-compiled Lua files and does not require LuaRocks or compilation." - @rm -f sentry-lua-sdk-publish.zip - @# Create temporary directory for packaging - @mkdir -p publish-temp - @# Copy required files - @cp README.md publish-temp/ || { echo "❌ README.md not found"; exit 1; } - @cp example-event.png publish-temp/ || { echo "❌ example-event.png not found"; exit 1; } - @cp CHANGELOG.md publish-temp/ || { echo "❌ CHANGELOG.md not found"; exit 1; } - @cp roblox.json publish-temp/ || { echo "❌ roblox.json not found"; exit 1; } - @# Copy build directory (recursively) - @cp -r build publish-temp/ || { echo "❌ build directory not found. Run 'make build' first."; exit 1; } - @# Copy examples directory (recursively) - @cp -r examples publish-temp/ || { echo "❌ examples directory not found"; exit 1; } - @# Create zip file - @cd publish-temp && zip -r ../sentry-lua-sdk-publish.zip . > /dev/null - @# Clean up temporary directory - @rm -rf publish-temp - @echo "✅ Publish package created: sentry-lua-sdk-publish.zip" - @echo "📦 This package is for direct download installation (Windows/cross-platform)" - @echo "📦 Contains pre-compiled Lua files - no LuaRocks or compilation required" - @echo "📦 Upload to GitHub Releases for user download" - @echo "" - @echo "Package contents:" - @unzip -l sentry-lua-sdk-publish.zip - -# Validate Roblox all-in-one integration file -roblox-all-in-one: build - @echo "Validating Roblox all-in-one integration..." - @./scripts/generate-roblox-all-in-one.sh - @echo "✅ Validated examples/roblox/sentry-all-in-one.lua" - @echo "📋 This file contains the complete SDK and can be copy-pasted into Roblox Studio" - diff --git a/README.md b/README.md index 2e6ea44..e6d92c1 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,25 @@ # Sentry SDK for Lua -> NOTE: This is a hackweek project and not officially supported by Sentry. -> Not all platforms are yet tested. -> -> We accept PRs if you're willing to fix bugs and add features. -> If there's enough interest, we could invest more into this. +> NOTE: Experimental SDK [![Tests](https://github.com/getsentry/sentry-lua/actions/workflows/test.yml/badge.svg)](https://github.com/getsentry/sentry-lua/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/getsentry/sentry-lua/branch/main/graph/badge.svg)](https://codecov.io/gh/getsentry/sentry-lua) -A platform-agnostic Sentry SDK for Lua environments. Written in Teal Language for better type safety and developer experience. - The goal of this SDK is to be *portable* Lua code, so CI/tests run on Standard Lua, as well as LuaJIT, which can run on [Game Consoles](https://luajit.org/status.html#consoles), one of [Sentry's latest platform investments](https://blog.sentry.io/playstation-xbox-switch-pc-or-mobile-wherever-youve-got-bugs-to-crush-sentry/). -![Screenshot of this example app](./example-event.png "Example Lua error with source context, logs and trace connected") - -## Features - -- **Platform Agnostic**: Works across Redis, nginx, Roblox, game engines, and standard Lua -- **Type Safe**: Written in Teal Language with full type definitions -- **Comprehensive**: Error tracking, breadcrumbs, context management, and scoped operations -- **Performance Monitoring**: Object-oriented API for transactions and spans with distributed tracing -- **Distributed Tracing**: Automatic trace propagation across service boundaries via HTTP headers -- **Structured Logging**: Built-in logging with batching, print hooks, and trace correlation -- **Extensible**: Pluggable transport system for different environments - -## Supported Platforms - -- Standard Lua 5.1+ environments -- Roblox game platform -- LÖVE 2D game engine -- Solar2D/Corona SDK -- Defold game engine -- Redis (lua scripting) -- nginx/OpenResty - ## Installation ### LuaRocks (macOS/Linux) ```bash -# Install from LuaRocks.org - requires Unix-like system for Teal compilation -luarocks install sentry/sentry +luarocks install sentry/sdk ``` -**Note:** Use `sentry/sentry` (not just `sentry`) as the plain `sentry` package is owned by someone else. +**Note:** Use `sentry/sdk` (not just `sentry`) as the plain `sentry` package is not a Sentry SDK. ### Direct Download (Windows/Cross-platform) For Windows or systems without make/compiler support: 1. Download the latest `sentry-lua-sdk-publish.zip` from [GitHub Releases](https://github.com/getsentry/sentry-lua/releases) 2. Extract the contents -3. Add the `build/sentry` directory to your Lua `package.path` -4. No compilation required - contains pre-built Lua files - -```lua --- Example usage after extracting to project directory -package.path = package.path .. ";./build/?.lua;./build/?/init.lua" -local sentry = require("sentry") -``` - -### Development (From Source) -```bash -# Clone the repository and build from source -git clone https://github.com/getsentry/sentry-lua.git -cd sentry-lua -make install # Install build dependencies -make build # Compile Teal sources to Lua -luarocks make --local # Install locally -``` - -### Platform-Specific Instructions - -#### Roblox -Import the module through the Roblox package system or use the pre-built releases. - -#### LÖVE 2D -The SDK automatically detects the Love2D environment and uses the lua-https module for reliable HTTPS transport. Use the direct download method and copy the SDK files into your Love2D project: - -```lua --- main.lua -local sentry = require("sentry") -local logger = require("sentry.logger") - -function love.load() - sentry.init({ - dsn = "https://your-dsn@sentry.io/project-id", - environment = "love2d", - release = "0.0.6" - }) - - -- Optional: Enable logging integration - logger.init({ - enable_logs = true, - max_buffer_size = 10, - flush_timeout = 5.0 - }) -end - -function love.update(dt) - -- Flush transport periodically - sentry.flush() -end - -function love.quit() - -- Clean shutdown - sentry.close() -end -``` - -**HTTPS Support**: The Love2D example includes a pre-compiled `https.so` binary from [lua-https](https://github.com/love2d/lua-https) for reliable SSL/TLS support. This binary is committed to the repository for convenience. If you need to rebuild it for your platform: - -```bash -cd examples/love2d/lua-https -cmake -Bbuild -S. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$PWD/install -cmake --build build --target install -# Copy install/https.so to your Love2D project -``` - -See `examples/love2d/` for a complete interactive demo with error triggering and visual feedback. ## Quick Start @@ -127,7 +30,7 @@ local sentry = require("sentry") sentry.init({ dsn = "https://your-dsn@sentry.io/project-id", environment = "production", - release = "0.0.6" + release = "0.0.7" }) -- Capture a message @@ -145,7 +48,7 @@ if not success then }) end --- Flush pending events immediately +-- Flush pending events sentry.flush() -- Clean shutdown @@ -166,253 +69,6 @@ sentry.close() - `sentry.with_scope(callback)` - Execute code with isolated scope - `sentry.wrap(main_function, error_handler)` - Wrap function with error handling -## Distributed Tracing - -The Sentry Lua SDK supports distributed tracing to improve debuggability across app and service boundaries. -Traces let you see how telemetry (errors/events, logs, etc) from different processes links together, making it easier to follow execution paths and uncover issues. -In addition, through spans, they highlight performance characteristics of your application, helping you detect and resolve bottlenecks. - -### Requirements - -For the distributed tracing examples, you'll need additional dependencies: - -```bash -luarocks install pegasus # HTTP server framework -luarocks install luasocket # HTTP client library -``` - -### Basic Tracing - -```lua -local sentry = require("sentry") -local performance = require("sentry.performance") - -sentry.init({ - dsn = "https://your-dsn@sentry.io/project-id" -}) - --- Start a transaction -local transaction = performance.start_transaction("user_checkout", "http.server") - --- Add spans for different operations -local validation_span = transaction:start_span("validation", "Validate cart") --- ... validation logic ... -validation_span:finish("ok") - -local payment_span = transaction:start_span("payment.charge", "Process payment") --- ... payment logic ... -payment_span:finish("ok") - --- Finish the transaction -transaction:finish("ok") -``` - -### Advanced Tracing Features - -#### Nested Spans and Context Management - -```lua -local transaction = performance.start_transaction("api_request", "http.server") - --- Nested spans automatically maintain parent-child relationships -local db_span = transaction:start_span("db.query", "Get user data") -local user_id = get_user_from_db() -db_span:finish("ok") - --- Spans can be nested further -local processing_span = transaction:start_span("processing", "Process user data") - -local validation_span = processing_span:start_span("validation", "Validate permissions") -validate_user_permissions(user_id) -validation_span:finish("ok") - -local enrichment_span = processing_span:start_span("enrichment", "Enrich user data") -enrich_user_data(user_id) -enrichment_span:finish("ok") - -processing_span:finish("ok") -transaction:finish("ok") -``` - -#### Adding Context and Tags - -```lua -local transaction = performance.start_transaction("checkout", "business.process") - --- Add tags and data to transactions -transaction:add_tag("user_type", "premium") -transaction:add_tag("payment_method", "credit_card") -transaction:add_data("cart_items", 3) -transaction:add_data("total_amount", 99.99) - -local span = transaction:start_span("payment.process", "Charge credit card") --- Add context to individual spans -span:add_tag("processor", "stripe") -span:add_data("amount", 99.99) -span:add_data("currency", "USD") - --- Finish with status -span:finish("ok") -- or "error", "cancelled", etc. -transaction:finish("ok") -``` - -### Distributed Tracing Examples - -The SDK includes complete examples demonstrating distributed tracing: - -- `examples/tracing/client.lua` - HTTP client that propagates trace context -- `examples/tracing/server.lua` - HTTP server with distributed tracing endpoints - -To see distributed tracing in action: - -1. Start the server: `lua examples/tracing/server.lua` -2. In another terminal, run the client: `lua examples/tracing/client.lua` -3. Check your Sentry dashboard to see connected traces across both processes - -The examples demonstrate: -- Object-oriented API with method chaining on transactions and spans -- Automatic trace context propagation via HTTP headers (`sentry-trace`, `baggage`) -- Proper parent-child span relationships across service boundaries -- Error correlation within distributed traces -- Performance monitoring with detailed timing information - -#### Manual Trace Propagation - -For custom HTTP implementations or other transport mechanisms: - -```lua -local tracing = require("sentry.tracing") - --- Get trace headers for outgoing requests -local headers = tracing.get_request_headers("https://api.example.com") --- headers will contain sentry-trace and baggage headers - --- Continue trace from incoming headers on the receiving side -local incoming_headers = { - ["sentry-trace"] = "abc123-def456-1", - ["baggage"] = "user_id=12345,environment=prod" -} -tracing.continue_trace_from_request(incoming_headers) - --- Now start transaction with continued trace context -local transaction = performance.start_transaction("api_handler", "http.server") --- This transaction will be part of the distributed trace -``` - -**Benefits of the new API:** -- **No global state**: Each transaction and span is explicitly managed -- **Better control**: Clear ownership of when operations start and finish -- **Proper nesting**: Spans automatically inherit from their parent transaction/span -- **Type safety**: Full Teal type definitions for all methods -- **Easier debugging**: No hidden global state to track - -## Structured Logging - -The Sentry Lua SDK provides comprehensive logging capabilities with automatic batching, trace correlation, and print statement capture. - -### Basic Logging - -```lua -local sentry = require("sentry") -local logger = require("sentry.logger") - -sentry.init({ - dsn = "https://your-dsn@sentry.io/project-id", - _experiments = { - enable_logs = true, - hook_print = true -- Automatically capture print() calls - } -}) - --- Initialize logger -logger.init({ - enable_logs = true, - max_buffer_size = 100, - flush_timeout = 5.0, - hook_print = true -}) - --- Log at different levels -logger.trace("Fine-grained debugging information") -logger.debug("Debugging information") -logger.info("General information") -logger.warn("Warning message") -logger.error("Error occurred") -logger.fatal("Critical failure") -``` - -### Structured Logging with Parameters - -```lua --- Parameterized messages for better searchability -local user_id = "user_123" -local order_id = "order_456" - -logger.info("User %s placed order %s", {user_id, order_id}) -logger.error("Payment failed for user %s with error %s", {user_id, "CARD_DECLINED"}) - --- With additional attributes -logger.info("Order completed successfully", {user_id, order_id}, { - order_total = 149.99, - payment_method = "credit_card", - shipping_type = "express", - processing_time = 1.2 -}) -``` - -### Automatic Print Capture - -```lua --- These print statements are automatically captured as logs -print("Application started") -- Becomes info-level log -print("Debug:", user_id, order_id) -- Multiple arguments handled - --- No infinite loops - Sentry's own print statements are ignored -``` - -### Log Correlation with Traces - -```lua -local transaction = performance.start_transaction("checkout", "business_process") - --- Logs within transactions are automatically correlated -logger.info("Starting checkout process") - -local span = transaction:start_span("validation", "Validate cart") -logger.debug("Validating cart for user %s", {user_id}) -span:finish("ok") - -logger.warn("Payment processor slow: %sms", {2100}) -transaction:finish("ok") - --- All logs will have trace_id linking them to the transaction -``` - -### Advanced Configuration - -```lua -logger.init({ - enable_logs = true, - max_buffer_size = 50, -- Batch up to 50 logs - flush_timeout = 10.0, -- Flush every 10 seconds - hook_print = true, -- Capture print statements - - before_send_log = function(log_record) - -- Filter sensitive information - if log_record.body:find("password") then - return nil -- Don't send this log - end - - -- Add custom attributes - log_record.attributes["custom_field"] = { - value = "custom_value", - type = "string" - } - - return log_record - end -}) -``` ## Automatic Error Capture @@ -465,34 +121,38 @@ For example, the LÖVE framework example app: ## Development -### Prerequisites -- Teal Language compiler -- busted (for testing) -- Docker (for integration tests) +This project uses a cross-platform Lua development script instead of Make for better Windows compatibility. -### Building -```bash -make install # Install dependencies -make build # Compile Teal to Lua -make test # Run unit tests -make docs # Generate documentation -make serve-docs # Serve docs at http://localhost:8000 -make lint-soft # Lint with warnings (permissive) -``` +### Setup + +1. Install Lua and LuaRocks +2. Install dependencies: `lua scripts/dev.lua install` + +### Common Tasks -### Testing ```bash -# Unit tests -make test +# Run tests +lua scripts/dev.lua test + +# Run linter +lua scripts/dev.lua lint -# Docker integration tests -make docker-test-redis -make docker-test-nginx +# Format code +lua scripts/dev.lua format -# Full test suite -make test-all +# Run full CI pipeline locally +lua scripts/dev.lua ci + +# Show all available commands +lua scripts/dev.lua help ``` -## License +### Code Quality + +- **Linting**: luacheck for code quality +- **Formatting**: StyLua for consistent style +- **Testing**: busted for test framework +- **Coverage**: luacov for coverage reporting + +All tools run cross-platform via the `scripts/dev.lua` script. -MIT License - see LICENSE file for details. diff --git a/codecov.yml b/codecov.yml index ae1aba8..e1102e0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,9 +14,7 @@ coverage: ignore: - "spec/" - "examples/" - - "build/" - "*.rockspec" - - "Makefile" - "*.md" parsers: diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile deleted file mode 100644 index 2daad11..0000000 --- a/docker/nginx/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM openresty/openresty:alpine - -RUN apk add --no-cache \ - luarocks \ - build-base \ - unzip - -RUN luarocks install lua-cjson -RUN luarocks install lua-resty-http - -WORKDIR /app - -COPY . . -COPY docker/nginx/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf -COPY docker/nginx/test.lua /usr/local/openresty/nginx/html/test.lua - -RUN if [ -f tlconfig.lua ]; then \ - luarocks install tl && \ - tl build; \ - fi - -EXPOSE 80 - -CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker/nginx/docker-compose.yml b/docker/nginx/docker-compose.yml deleted file mode 100644 index 07c193b..0000000 --- a/docker/nginx/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: '3.8' - -services: - nginx: - build: - context: ../.. - dockerfile: docker/nginx/Dockerfile - ports: - - "8080:80" - volumes: - - ../../:/app - working_dir: /app \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf deleted file mode 100644 index fe56953..0000000 --- a/docker/nginx/nginx.conf +++ /dev/null @@ -1,21 +0,0 @@ -worker_processes 1; -error_log logs/error.log; -events { - worker_connections 1024; -} - -http { - lua_package_path "/app/build/?.lua;/app/src/?.lua;;"; - - server { - listen 80; - - location /test { - content_by_lua_file /usr/local/openresty/nginx/html/test.lua; - } - - location /health { - return 200 "OK"; - } - } -} \ No newline at end of file diff --git a/docker/nginx/test.lua b/docker/nginx/test.lua deleted file mode 100644 index f979c9e..0000000 --- a/docker/nginx/test.lua +++ /dev/null @@ -1,43 +0,0 @@ -local sentry = require("sentry.init") - -ngx.say("Testing Sentry SDK with nginx/OpenResty integration...") - -local sentry_client = sentry.init({ - dsn = "https://test@sentry.io/123456", - environment = "docker-nginx-test", - debug = true -}) - -ngx.say("✓ Sentry initialized") - -sentry.set_tag("platform", "nginx") -sentry.set_tag("server", ngx.var.server_name or "localhost") -sentry.set_extra("request_uri", ngx.var.request_uri) -sentry.set_extra("user_agent", ngx.var.http_user_agent) - -sentry.add_breadcrumb({ - message = "HTTP request received", - category = "http", - level = "info", - data = { - method = ngx.var.request_method, - uri = ngx.var.request_uri - } -}) - -local event_id = sentry.capture_message("Hello from nginx integration!", "info") -ngx.say("✓ Message captured: " .. event_id) - -local success, err = pcall(function() - error("Test nginx error") -end) - -if not success then - local exception_id = sentry.capture_exception({ - type = "NginxTestError", - message = err - }) - ngx.say("✓ Exception captured: " .. exception_id) -end - -ngx.say("✓ nginx integration test completed successfully!") \ No newline at end of file diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile deleted file mode 100644 index 797ee45..0000000 --- a/docker/redis/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM openresty/openresty:alpine - -RUN apk add --no-cache \ - luarocks \ - build-base \ - unzip - -RUN luarocks install lua-cjson -RUN luarocks install luasocket -RUN luarocks install redis-lua - -WORKDIR /app - -COPY . . - -RUN if [ -f tlconfig.lua ]; then \ - luarocks install tl && \ - tl build; \ - fi \ No newline at end of file diff --git a/docker/redis/docker-compose.yml b/docker/redis/docker-compose.yml deleted file mode 100644 index 520cb02..0000000 --- a/docker/redis/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.8' - -services: - redis: - image: redis:7-alpine - ports: - - "6379:6379" - command: redis-server --appendonly yes - - sentry-test: - build: - context: ../.. - dockerfile: docker/redis/Dockerfile - depends_on: - - redis - environment: - - REDIS_HOST=redis - - REDIS_PORT=6379 - volumes: - - ../../:/app - working_dir: /app - command: lua docker/redis/test.lua \ No newline at end of file diff --git a/docker/redis/test.lua b/docker/redis/test.lua deleted file mode 100644 index d57d236..0000000 --- a/docker/redis/test.lua +++ /dev/null @@ -1,207 +0,0 @@ -local redis = require("redis") -local sentry = require("sentry.init") - -print("=== Testing Sentry SDK with Redis Lua Scripts ===") - --- Connect to Redis -local client = redis.connect("redis", 6379) -if not client then - print("❌ Failed to connect to Redis") - os.exit(1) -end - -print("✓ Connected to Redis") - --- Initialize Sentry -local sentry_client = sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "docker-redis-test", - debug = true -}) - -print("✓ Sentry initialized") - --- Set tags and context -sentry.set_tag("platform", "redis") -sentry.set_tag("test_type", "lua_script") -sentry.set_extra("redis_info", "Docker test with Lua scripts") - -sentry.add_breadcrumb({ - message = "Redis connection established", - category = "redis", - level = "info" -}) - --- Test 1: Basic Redis operations with Sentry tracking -print("\n--- Test 1: Basic Redis Operations ---") -sentry.add_breadcrumb({ - message = "Starting basic Redis operations test", - category = "test", - level = "info" -}) - -client:set("test:key", "hello_redis") -local value = client:get("test:key") -print("✓ SET/GET test: " .. (value or "nil")) - --- Test 2: Load and execute a Lua script in Redis -print("\n--- Test 2: Redis Lua Script Execution ---") -sentry.add_breadcrumb({ - message = "Loading Lua script into Redis", - category = "redis", - level = "info" -}) - --- Lua script that increments a counter and logs activity -local lua_script = [[ - local key = KEYS[1] - local increment = tonumber(ARGV[1]) or 1 - local current = redis.call('GET', key) - - if not current then - current = 0 - else - current = tonumber(current) - end - - local new_value = current + increment - redis.call('SET', key, new_value) - - -- Log the operation - local log_key = key .. ':log' - redis.call('LPUSH', log_key, 'Incremented from ' .. current .. ' to ' .. new_value) - redis.call('EXPIRE', log_key, 300) -- Expire after 5 minutes - - return {new_value, current} -]] - --- Load the script and get its SHA1 hash -local script_sha = client:script_load(lua_script) -print("✓ Lua script loaded: " .. script_sha) - --- Execute the script multiple times -for i = 1, 5 do - local result = client:evalsha(script_sha, 1, "test:counter", i) - print(" Execution " .. i .. ": counter = " .. result[1] .. " (was " .. result[2] .. ")") - - sentry.add_breadcrumb({ - message = "Script execution " .. i .. " completed", - category = "redis", - level = "debug", - data = { - counter_value = result[1], - increment = i - } - }) -end - --- Check the log entries -local logs = client:lrange("test:counter:log", 0, -1) -print("✓ Script execution log:") -for i, log_entry in ipairs(logs) do - print(" " .. i .. ". " .. log_entry) -end - --- Test 3: Error handling in Redis Lua script -print("\n--- Test 3: Error Handling in Lua Scripts ---") -local error_script = [[ - local key = KEYS[1] - if key == "error" then - error("Intentional error in Lua script") - end - return "success" -]] - -local error_script_sha = client:script_load(error_script) - --- Test successful execution -local success_result = client:evalsha(error_script_sha, 1, "success") -print("✓ Success case: " .. success_result) - --- Test error case with Sentry error capture -local success, err = pcall(function() - return client:evalsha(error_script_sha, 1, "error") -end) - -if not success then - print("✓ Caught Redis Lua script error: " .. err) - local exception_id = sentry.capture_exception({ - type = "RedisLuaScriptError", - message = "Redis Lua script execution failed: " .. err - }) - print("✓ Exception sent to Sentry: " .. exception_id) -end - --- Test 4: Complex Redis operations with transaction -print("\n--- Test 4: Complex Operations with Transaction ---") -sentry.add_breadcrumb({ - message = "Starting Redis transaction test", - category = "redis", - level = "info" -}) - --- Multi-operation Lua script (atomic transaction) -local transaction_script = [[ - local user_key = KEYS[1] - local session_key = KEYS[2] - local user_data = ARGV[1] - local session_id = ARGV[2] - - -- Set user data - redis.call('HSET', user_key, 'data', user_data, 'last_seen', ARGV[3]) - - -- Create session - redis.call('SET', session_key, session_id) - redis.call('EXPIRE', session_key, 3600) -- 1 hour - - -- Update activity counter - local activity_key = 'activity:' .. ARGV[4] - local count = redis.call('INCR', activity_key) - - return { - redis.call('HGETALL', user_key), - redis.call('GET', session_key), - count - } -]] - -local tx_script_sha = client:script_load(transaction_script) -local timestamp = os.time() -local result = client:evalsha(tx_script_sha, 2, - "user:123", "session:abc", - "test_user_data", "session_abc_123", timestamp, "daily") - -print("✓ Transaction completed:") -print(" User data: " .. table.concat(result[1], ", ")) -print(" Session: " .. result[2]) -print(" Activity count: " .. result[3]) - --- Clean up -client:del("test:key", "test:counter", "test:counter:log", "user:123", "session:abc", "activity:daily") -print("✓ Cleanup completed") - --- Final Sentry message -local final_event_id = sentry.capture_message("Redis Lua script integration test completed successfully!", "info") -print("\n✓ Final test report sent to Sentry: " .. final_event_id) - --- Test general Sentry error handling -local success, err = pcall(function() - error("Test error for stack trace verification") -end) - -if not success then - local exception_id = sentry.capture_exception({ - type = "TestError", - message = err, - extra = { - test_phase = "final_error_test", - redis_connected = true - } - }) - print("✓ Test exception captured: " .. exception_id) -end - -print("\n=== All Redis + Sentry integration tests completed successfully! ===") - --- Close Redis connection -client:quit() \ No newline at end of file diff --git a/example-event.png b/example-event.png deleted file mode 100644 index 440c493..0000000 Binary files a/example-event.png and /dev/null differ diff --git a/examples/basic.lua b/examples/basic.lua index ae2354a..1c25ed1 100644 --- a/examples/basic.lua +++ b/examples/basic.lua @@ -1,24 +1,19 @@ --- NOTE: Before running this example, compile the Teal files into Lua - -package.path = "build/?.lua;build/?/init.lua;" .. package.path -local sentry = require("sentry.init") +package.path = "src/?/init.lua;" .. package.path +local sentry = require("sentry") -- Initialize Sentry with your DSN sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "production", - release = "wrap-demo@1.0", - debug = true, - -- file_transport = true, - -- file_path = "sentry-events.log", - -- append_mode = true + dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", + environment = "production", + release = "wrap-demo@1.0", + debug = true, }) -- Set user context sentry.set_user({ - id = "user123", - email = "user@example.com", - username = "testuser" + id = "user123", + email = "user@example.com", + username = "testuser", }) -- Add tags for filtering @@ -31,31 +26,27 @@ sentry.set_extra("request_id", "req456") -- Add breadcrumbs for debugging context sentry.add_breadcrumb({ - message = "User started checkout process", - category = "navigation", - level = "info" + message = "User started checkout process", + category = "navigation", + level = "info", }) -- Helper functions to create deeper stack traces with parameters -local function send_confirmation_email(user_email, order_id, amount) - sentry.capture_message("Purchase confirmation sent to " .. user_email .. " for order #" .. order_id, "info") -end +local function send_confirmation_email(user_email, order_id, amount) sentry.capture_message("Purchase confirmation sent to " .. user_email .. " for order #" .. order_id, "info") end local function process_payment(payment_method, amount, currency) - local order_id = "ORD-" .. math.random(1000, 9999) - send_confirmation_email("customer@example.com", order_id, amount) + local order_id = "ORD-" .. math.random(1000, 9999) + send_confirmation_email("customer@example.com", order_id, amount) end local function validate_cart(items_count, total_price) - if items_count > 0 then - process_payment("credit_card", total_price, "USD") - end + if items_count > 0 then process_payment("credit_card", total_price, "USD") end end local function checkout_handler(user_id, session_token) - local cart_items = 3 - local total = 149.99 - validate_cart(cart_items, total) + local cart_items = 3 + local total = 149.99 + validate_cart(cart_items, total) end -- Capture a message with multiple stack frames and parameters @@ -65,180 +56,164 @@ checkout_handler("user_12345", "sess_abcdef123456") -- Common pitfall: Using 0-based indexing (Lua uses 1-based) local function array_access_error(department, min_users_required) - local users = {"alice", "bob", "charlie"} - local debug_info = "Checking " .. department .. " (need " .. min_users_required .. " users)" - - -- This will access nil (common mistake from other languages) - local first_user = users[0] -- Should be users[1] - - -- This will cause an error when trying to use nil - return string.upper(first_user) .. " in " .. debug_info + local users = { "alice", "bob", "charlie" } + local debug_info = "Checking " .. department .. " (need " .. min_users_required .. " users)" + + -- This will access nil (common mistake from other languages) + local first_user = users[0] -- Should be users[1] + + -- This will cause an error when trying to use nil + return string.upper(first_user) .. " in " .. debug_info end -- Common pitfall: Using + for string concatenation instead of .. local function string_concat_error(greeting_type, username, user_id) - local timestamp = os.time() - local session_id = "sess_" .. math.random(1000, 9999) - - -- Wrong: using + instead of .. for concatenation - local message = greeting_type + " " + username + " (ID: " + user_id + ") at " + timestamp - return message .. " session: " .. session_id + local timestamp = os.time() + local session_id = "sess_" .. math.random(1000, 9999) + + -- Wrong: using + instead of .. for concatenation + local message = greeting_type + " " + username + " (ID: " + user_id + ") at " + timestamp + return message .. " session: " .. session_id end -- Common pitfall: Calling methods on nil values local function nil_method_call(service_name, environment) - local config = nil -- Simulate missing configuration - local default_timeout = 30 - local retry_count = 3 - - -- This will error: attempt to index a nil value - local timeout = config.timeout or default_timeout - return "Service " .. service_name .. " (" .. environment .. ") timeout: " .. timeout + local config = nil -- Simulate missing configuration + local default_timeout = 30 + + -- This will error: attempt to index a nil value + local timeout = config.timeout or default_timeout + return "Service " .. service_name .. " (" .. environment .. ") timeout: " .. timeout end -- Common pitfall: Incorrect table iteration local function table_iteration_error(record_type, max_items) - local data = {name = "test_record", value = 42, priority = "high"} - local result = "" - local processed_count = 0 - - -- Wrong: using ipairs on a hash table (should use pairs) - for i, v in ipairs(data) do - result = result .. tostring(v) - processed_count = processed_count + 1 - end - - return record_type .. ": processed " .. processed_count .. "/" .. max_items .. " -> " .. result + local data = { name = "test_record", value = 42, priority = "high" } + local result = "" + local processed_count = 0 + + -- Wrong: using ipairs on a hash table (should use pairs) + for i, v in ipairs(data) do + result = result .. tostring(v) + processed_count = processed_count + 1 + end + + return record_type .. ": processed " .. processed_count .. "/" .. max_items .. " -> " .. result end -- Demonstrate each error type with proper context sentry.add_breadcrumb({ - message = "Starting error demonstration", - category = "demo", - level = "info" + message = "Starting error demonstration", + category = "demo", + level = "info", }) -- Test array indexing error sentry.with_scope(function(scope) - scope:set_tag("error_type", "array_indexing") - scope:set_extra("expected_behavior", "Lua arrays are 1-indexed, not 0-indexed") - - local function safe_array_access() - array_access_error("engineering", 2) - end - - xpcall(safe_array_access, function(err) - sentry.capture_exception({ - type = "IndexError", - message = "Array indexing error: " .. tostring(err) - }) - return err - end) + scope:set_tag("error_type", "array_indexing") + scope:set_extra("expected_behavior", "Lua arrays are 1-indexed, not 0-indexed") + + local function safe_array_access() array_access_error("engineering", 2) end + + xpcall(safe_array_access, function(err) + sentry.capture_exception({ + type = "IndexError", + message = "Array indexing error: " .. tostring(err), + }) + return err + end) end) -- Test string concatenation error sentry.with_scope(function(scope) - scope:set_tag("error_type", "string_concatenation") - scope:set_extra("expected_behavior", "Lua uses .. for string concatenation, not +") - - local function safe_string_concat() - string_concat_error("Hello", "john_doe", 42) - end - - xpcall(safe_string_concat, function(err) - sentry.capture_exception({ - type = "TypeError", - message = "String concatenation error: " .. tostring(err) - }) - return err - end) + scope:set_tag("error_type", "string_concatenation") + scope:set_extra("expected_behavior", "Lua uses .. for string concatenation, not +") + + local function safe_string_concat() string_concat_error("Hello", "john_doe", 42) end + + xpcall(safe_string_concat, function(err) + sentry.capture_exception({ + type = "TypeError", + message = "String concatenation error: " .. tostring(err), + }) + return err + end) end) -- Test nil method call error sentry.with_scope(function(scope) - scope:set_tag("error_type", "nil_access") - scope:set_extra("expected_behavior", "Always check for nil before accessing table fields") - - local function safe_nil_access() - nil_method_call("database", "production") - end - - xpcall(safe_nil_access, function(err) - sentry.capture_exception({ - type = "NilAccessError", - message = "Nil access error: " .. tostring(err) - }) - return err - end) + scope:set_tag("error_type", "nil_access") + scope:set_extra("expected_behavior", "Always check for nil before accessing table fields") + + local function safe_nil_access() nil_method_call("database", "production") end + + xpcall(safe_nil_access, function(err) + sentry.capture_exception({ + type = "NilAccessError", + message = "Nil access error: " .. tostring(err), + }) + return err + end) end) -- Test table iteration error (this one might not error but produces wrong results) sentry.with_scope(function(scope) - scope:set_tag("error_type", "iteration_logic") - scope:set_extra("expected_behavior", "Use pairs() for hash tables, ipairs() for arrays") - - local result = table_iteration_error("user_profile", 10) - if result:find("processed 0/") then - sentry.capture_message("Table iteration produced empty result - likely using wrong iterator", "warning") - end + scope:set_tag("error_type", "iteration_logic") + scope:set_extra("expected_behavior", "Use pairs() for hash tables, ipairs() for arrays") + + local result = table_iteration_error("user_profile", 10) + if result:find("processed 0/") then sentry.capture_message("Table iteration produced empty result - likely using wrong iterator", "warning") end end) -- Original database error for comparison with parameters local function database_query(query_type, table_name, timeout_ms) - local connection_attempts = 3 - local last_error = "Connection timeout after " .. timeout_ms .. "ms" - error("Database query failed: " .. query_type .. " on " .. table_name .. " (" .. last_error .. ")") + local last_error = "Connection timeout after " .. timeout_ms .. "ms" + error("Database query failed: " .. query_type .. " on " .. table_name .. " (" .. last_error .. ")") end -local function fetch_user_data(user_id, include_preferences) - database_query("SELECT", "users", 5000) -end +local function fetch_user_data(user_id, include_preferences) database_query("SELECT", "users", 5000) end -local function authenticate_user(username, password_hash) - fetch_user_data("user_12345", true) -end +local function authenticate_user(username, password_hash) fetch_user_data("user_12345", true) end -local function handle_request(request_id, client_ip) - authenticate_user("john.doe", "sha256_abc123") -end +local function handle_request(request_id, client_ip) authenticate_user("john.doe", "sha256_abc123") end -- Capture the original authentication error local function error_handler(err) - sentry.capture_exception({ - type = "AuthenticationError", - message = err - }) - return err + sentry.capture_exception({ + type = "AuthenticationError", + message = err, + }) + return err end xpcall(function() handle_request("req_789", "192.168.1.100") end, error_handler) -- Demonstrate automatic error capture vs manual handling sentry.add_breadcrumb({ - message = "About to demonstrate automatic vs manual error handling", - category = "demo", - level = "info" + message = "About to demonstrate automatic vs manual error handling", + category = "demo", + level = "info", }) -- Example 1: Manual error handling with xpcall (traditional approach) print("\n=== Manual Error Handling ===") sentry.with_scope(function(scope) - scope:set_tag("handling_method", "manual") - - local function manual_error_demo(operation_type, resource_id) - local data = nil - local context = "Processing " .. operation_type .. " for resource " .. resource_id - return data.missing_field .. " (" .. context .. ")" -- Will cause nil access error - end - - xpcall(function() manual_error_demo("update", "res_456") end, function(err) - sentry.capture_exception({ - type = "ManuallyHandledError", - message = "Manually captured: " .. tostring(err) - }) - print("[Manual] Error captured and handled gracefully") - return err - end) + scope:set_tag("handling_method", "manual") + + local function manual_error_demo(operation_type, resource_id) + local data = nil + local context = "Processing " .. operation_type .. " for resource " .. resource_id + return data.missing_field .. " (" .. context .. ")" -- Will cause nil access error + end + + xpcall(function() manual_error_demo("update", "res_456") end, function(err) + sentry.capture_exception({ + type = "ManuallyHandledError", + message = "Manually captured: " .. tostring(err), + }) + print("[Manual] Error captured and handled gracefully") + return err + end) end) -- Example 2: Automatic error capture (new functionality) @@ -246,22 +221,22 @@ end) print("\n=== Automatic Error Handling ===") print("The following error will be automatically captured by Sentry:") sentry.with_scope(function(scope) - scope:set_tag("handling_method", "automatic") - scope:set_extra("note", "This error is automatically captured without xpcall") - - -- Uncomment the next line to test automatic capture - -- WARNING: This will terminate the program! - -- error("This error is automatically captured!") - - print("[Automatic] Error capture is enabled - any unhandled error() calls are automatically sent to Sentry") + scope:set_tag("handling_method", "automatic") + scope:set_extra("note", "This error is automatically captured without xpcall") + + -- Uncomment the next line to test automatic capture + -- WARNING: This will terminate the program! + -- error("This error is automatically captured!") + + print("[Automatic] Error capture is enabled - any unhandled error() calls are automatically sent to Sentry") end) -- Use scoped context for temporary changes sentry.with_scope(function(scope) - scope:set_tag("temporary", "value") - sentry.capture_message("OS Detection Test Message", "warning") - -- Temporary context is automatically restored + scope:set_tag("temporary", "value") + sentry.capture_message("OS Detection Test Message", "warning") + -- Temporary context is automatically restored end) -- Clean up -sentry.close() \ No newline at end of file +sentry:close() diff --git a/examples/logging.lua b/examples/logging.lua deleted file mode 100644 index 4ce30a1..0000000 --- a/examples/logging.lua +++ /dev/null @@ -1,347 +0,0 @@ -#!/usr/bin/env lua - --- Example demonstrating Sentry logging functionality --- This shows structured logging, print hooks, and log batching - --- Add build path for modules -package.path = "build/?.lua;build/?/init.lua;" .. package.path - -local sentry = require("sentry") -local logger = require("sentry.logger") -local performance = require("sentry.performance") - --- Initialize Sentry with logging enabled -sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "development", - release = "logging-example@1.0.0", - debug = true, - - -- Experimental logging configuration - _experiments = { - enable_logs = true, - max_buffer_size = 5, -- Small buffer for demo - flush_timeout = 3.0, -- Quick flush for demo - hook_print = true -- Hook print statements - } -}) - --- Initialize logger with configuration -logger.init({ - enable_logs = true, - max_buffer_size = 5, - flush_timeout = 3.0, - hook_print = true, - before_send_log = function(log_record) - -- Example log filtering/modification - if log_record.level == "debug" and log_record.body:find("sensitive") then - print("[Logger] Filtering sensitive debug log") - return nil -- Filter out sensitive logs - end - return log_record - end -}) - -print("=== Sentry Logging Example ===") -print("This example demonstrates various logging features\n") - --- Basic logging at different levels -print("1. Basic logging at different levels:") -logger.trace("This is a trace message for debugging") -logger.debug("User authentication process started") -logger.info("Application started successfully") -logger.warn("High memory usage detected: 85%") -logger.error("Failed to connect to external API") -logger.fatal("Critical system failure detected") - --- Structured logging with parameters -print("\n2. Structured logging with parameters:") -local user_id = "user_12345" -local action = "purchase" -local amount = 99.99 - -logger.info("User %s performed action %s with amount $%s", {user_id, action, amount}) -logger.warn("API endpoint %s response time %sms exceeds threshold", {"/api/users", 1500}) -logger.error("Database query failed for user %s with error %s", {user_id, "CONNECTION_TIMEOUT"}) - --- Logging with additional attributes -print("\n3. Logging with additional attributes:") -logger.info("Order processed successfully", nil, { - order_id = "order_789", - customer_type = "premium", - processing_time = 120, - payment_method = "credit_card", - is_express_shipping = true -}) - -logger.error("Payment processing failed", nil, { - error_code = "CARD_DECLINED", - attempts = 3, - amount = 149.99, - merchant_id = "merchant_456" -}) - --- Demonstrate print hook functionality -print("\n4. Print statements (automatically captured as logs):") -print("This is a regular print statement that will be logged") -print("Debug info:", "status=active", "count=42") -print("Multiple", "arguments", "in", "print", "statement") - --- Logging within transactions (trace correlation) -print("\n5. Logging within transaction context:") -local transaction = performance.start_transaction("logging_demo", "example") - -logger.info("Starting business process within transaction") - -local span = transaction:start_span("data_processing", "Process user data") -logger.debug("Processing user data for user %s", {user_id}) -logger.info("Data validation completed successfully") -span:finish("ok") - -local payment_span = transaction:start_span("payment", "Process payment") -logger.warn("Payment provider response time high: %sms", {2100}) -logger.error("Payment failed with error: %s", {"INSUFFICIENT_FUNDS"}, { - retry_count = 2, - fallback_available = true -}) -payment_span:finish("error") - -transaction:finish("error") - --- Demonstrate sensitive log filtering -print("\n6. Log filtering (sensitive logs are filtered out):") -logger.debug("Regular debug message") -logger.debug("This contains sensitive information and will be filtered") - --- Force flush before ending -print("\n7. Forcing log buffer flush:") -print("Current buffer status:", logger.get_buffer_status().logs, "logs pending") -logger.flush() - --- Demonstrate exception capture with log correlation -print("\n8. Exception capture with log correlation:") -logger.error("Critical error occurred in payment processing") -logger.info("Attempting to capture exception with context") - --- Create a realistic nested function scenario similar to basic example -local function validate_card_number(card_number, cvv_code) - logger.debug("Validating card number ending in %s", {string.sub(card_number, -4)}) - - -- Simulate card validation - if #card_number ~= 16 then - error("CardValidationError: Invalid card number length") - end - - if cvv_code < 100 or cvv_code > 999 then - error("CardValidationError: Invalid CVV code") - end - - logger.info("Card validation successful") - return true -end - -local function check_customer_limits(customer_id, amount, customer_tier) - logger.debug("Checking limits for customer %s (tier: %s)", {customer_id, customer_tier}) - - local daily_limits = { - bronze = 500, - silver = 2000, - gold = 10000, - platinum = 50000 - } - - local limit = daily_limits[customer_tier] or 100 - logger.info("Customer %s has daily limit of $%s", {customer_id, limit}) - - if amount > limit then - error("LimitExceededError: Transaction amount $" .. tostring(amount) .. " exceeds daily limit of $" .. tostring(limit)) - end - - return true -end - -local function validate_payment(customer_id, amount, payment_method, card_info) - logger.debug("Validating payment for customer %s", {customer_id}) - - if not customer_id or customer_id == "" then - error("ValidationError: Invalid customer ID provided") - end - - if amount <= 0 then - error("ValidationError: Invalid payment amount: $" .. tostring(amount)) - end - - -- Check customer limits first - check_customer_limits(customer_id, amount, card_info.tier or "bronze") - - -- Validate card if it's a credit card payment - if payment_method == "credit_card" then - validate_card_number(card_info.number, card_info.cvv) - - if amount > 5000 then - logger.warn("High value credit card transaction: $%s for customer %s", {amount, customer_id}) - end - end - - logger.info("Payment validation completed for customer %s", {customer_id}) - return true -end - -local function connect_to_gateway(gateway_config, retry_count) - logger.debug("Connecting to payment gateway (attempt %s)", {retry_count}) - - -- Simulate connection logic - if retry_count > 3 then - error("ConnectionError: Maximum retry attempts exceeded") - end - - logger.info("Successfully connected to payment gateway") - return {connection_id = "conn_" .. math.random(10000, 99999)} -end - -local function process_payment_request(gateway_connection, transaction_data, timeout_seconds) - logger.info("Processing payment request with timeout %s seconds", {timeout_seconds}) - - -- Simulate the actual payment processing error - if transaction_data.amount > 1000 then - logger.error("Payment gateway timeout after %s seconds", {timeout_seconds}) - error("PaymentGatewayTimeoutError: Connection failed after " .. tostring(timeout_seconds) .. " seconds") - end - - return { - status = "approved", - reference = "ref_" .. math.random(100000, 999999), - auth_code = "auth_" .. math.random(1000, 9999) - } -end - -local function process_transaction(order_data, gateway_config) - logger.info("Starting transaction processing for order %s", {order_data.order_id}) - - -- Add some structured data - logger.info("Order details", nil, { - order_id = order_data.order_id, - customer_id = order_data.customer_id, - amount = order_data.amount, - payment_method = order_data.payment_method, - merchant_id = "merchant_789" - }) - - -- Validate the payment first - validate_payment(order_data.customer_id, order_data.amount, order_data.payment_method, order_data.card_info) - - -- Connect to payment gateway - local connection = connect_to_gateway(gateway_config, 1) - logger.info("Using gateway connection %s", {connection.connection_id}) - - -- Process the actual payment - local payment_result = process_payment_request(connection, order_data, 30) - - logger.info("Transaction processed successfully with reference %s", {payment_result.reference}) - return { - status = "success", - transaction_id = "txn_" .. order_data.order_id, - payment_reference = payment_result.reference - } -end - -local function handle_customer_order(customer_id, items, payment_info, gateway_config) - logger.info("Processing customer order for %s with %s items", {customer_id, #items}) - - local order_data = { - order_id = "order_" .. math.random(1000, 9999), - customer_id = customer_id, - amount = payment_info.amount, - payment_method = payment_info.method, - items = items, - card_info = payment_info.card_info - } - - logger.debug("Created order data structure", nil, { - order_id = order_data.order_id, - item_count = #items, - total_amount = payment_info.amount - }) - - return process_transaction(order_data, gateway_config) -end - --- Simulate a realistic order scenario that will trigger the exception -local customer_id = "customer_12345" -local items = {"laptop", "mouse", "keyboard"} -local payment_info = { - amount = 1250, -- This will trigger the timeout error - method = "credit_card", - card_info = { - number = "4532123456789012", - cvv = 123, - tier = "gold" - } -} - -local gateway_config = { - url = "https://payment-gateway.example.com", - merchant_id = "merchant_789", - timeout = 30 -} - --- Capture exception with surrounding log context -logger.info("Starting customer order processing workflow") - --- Create a function chain that will trigger the error deeper in the stack -local function process_order_workflow(customer_id, items, payment_info, gateway_config) - logger.info("Initializing order workflow") - return handle_customer_order(customer_id, items, payment_info, gateway_config) -end - -local function execute_customer_transaction(customer_data, order_details, gateway_settings) - logger.debug("Executing customer transaction with gateway settings") - return process_order_workflow(customer_data.id, order_details.items, order_details.payment, gateway_settings) -end - -local function start_business_process(customer_info, order_info, payment_config) - logger.info("Starting business process for customer %s", {customer_info.id}) - return execute_customer_transaction(customer_info, order_info, payment_config) -end - --- Use the nested function chain to create multi-frame stack trace -local customer_data = {id = customer_id, tier = "gold"} -local order_details = {items = items, payment = payment_info} - --- Use xpcall with a custom error handler that captures the original stack trace -local function error_handler(err) - logger.error("Order processing failed with error: %s", {tostring(err)}) - logger.error("Failed order details", nil, { - customer_id = customer_id, - item_count = #items, - payment_amount = payment_info.amount, - error_location = "payment_processing" - }) - - -- Capture the exception, this will use the current stack trace from where the error occurred - sentry.capture_exception({ - type = "PaymentGatewayTimeoutError", - message = tostring(err) - }) - logger.info("Exception captured and sent to Sentry with full context") - return err -end - -local success, result = xpcall(function() - return start_business_process(customer_data, order_details, gateway_config) -end, error_handler) - -if success then - logger.info("Order completed successfully: %s", {result.transaction_id}) -end - --- Wait a moment for async operations -print("\nWaiting for logs and exceptions to be sent...") -os.execute("sleep 3") - -print("\nExample completed! Check your Sentry project for:") -print("- Structured logs with parameters and attributes") -print("- Print statement captures") -print("- Exception with correlated log messages") -print("- Correlation with transaction traces") -print("- Different log levels and severity") \ No newline at end of file diff --git a/examples/logging_simple.lua b/examples/logging_simple.lua deleted file mode 100644 index 2756e4b..0000000 --- a/examples/logging_simple.lua +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env lua - --- Simple logging example that demonstrates functionality without network issues --- This shows the logging API works correctly even when transport fails - --- Add build path for modules -package.path = "build/?.lua;build/?/init.lua;" .. package.path - -local sentry = require("sentry") -local logger = require("sentry.logger") - -print("=== Simple Sentry Logging Demo ===\n") - --- Initialize Sentry -sentry.init({ - dsn = "https://6e6b321e9b334de79f0d56c54a0e2d94@o4505842095628288.ingest.us.sentry.io/4508485766955008", - debug = true -}) - --- Initialize logger with small buffer for immediate demonstration -logger.init({ - enable_logs = true, - max_buffer_size = 3, -- Very small buffer to trigger frequent flushes - flush_timeout = 1.0, -- Quick timeout - hook_print = false -- Disable print hooking for cleaner output -}) - -print("1. Testing basic logging levels:") -logger.info("Application started successfully") -logger.warn("This is a warning message") -logger.error("This is an error message") - --- Should auto-flush here due to buffer size (3) -print(" -> Buffer should have flushed automatically\n") - -print("2. Testing structured logging:") -logger.info("User %s logged in from %s", {"john_doe", "192.168.1.100"}) -logger.error("Database query failed: %s", {"Connection timeout"}) - -print(" -> Another flush should happen\n") - -print("3. Testing with attributes:") -logger.info("Order processed", nil, { - order_id = "ORD-12345", - amount = 99.99, - currency = "USD", - success = true -}) - -print(" -> One more flush\n") - -print("4. Buffer status check:") -local status = logger.get_buffer_status() -print(" Current buffer:", status.logs, "logs pending") -print(" Max buffer size:", status.max_size) - -print("\n5. Manual flush:") -logger.flush() -print(" -> Manually flushed remaining logs") - -print("\n=== Demo Complete ===") -print("The logging system is working correctly!") -print("Network errors are expected since logging is experimental in Sentry.") -print("The important thing is that logs are being:") -print("- ✅ Properly formatted") -print("- ✅ Batched correctly") -print("- ✅ Sent to transport layer") -print("- ✅ Structured with parameters and attributes") \ No newline at end of file diff --git a/examples/love2d/README.md b/examples/love2d/README.md index dda7bd2..1f457c5 100644 --- a/examples/love2d/README.md +++ b/examples/love2d/README.md @@ -88,10 +88,8 @@ The demo sends data to the Sentry playground project. In production, you would: When integrating Sentry into your Love2D game: -1. **Add SDK to Project**: Copy `build/sentry/` into your Love2D project +1. **Add SDK to Project**: Copy `src/sentry/` into your Love2D project 2. **Initialize Early**: Call `sentry.init()` in `love.load()` -3. **Flush Regularly**: Call `transport:flush()` in `love.update()` -4. **Clean Shutdown**: Call `transport:close()` in `love.quit()` 5. **Add Context**: Use breadcrumbs and tags for better debugging ### Clean API Usage diff --git a/examples/love2d/conf.lua b/examples/love2d/conf.lua index aaad323..64ab480 100644 --- a/examples/love2d/conf.lua +++ b/examples/love2d/conf.lua @@ -1,54 +1,54 @@ -- Love2D configuration for Sentry demo function love.conf(t) - t.identity = "sentry-love2d-demo" -- Save directory name - t.appendidentity = false -- Search files in source directory before save directory - t.version = "11.5" -- LÖVE version this game was made for - t.console = false -- Attach a console (Windows only) - t.accelerometerjoystick = false -- Enable accelerometer on mobile devices - t.externalstorage = false -- True to save files outside of the app folder - t.gammacorrect = false -- Enable gamma-correct rendering - - t.audio.mic = false -- Request microphone permission - t.audio.mixwithsystem = true -- Keep background music playing - - t.window.title = "Love2D Sentry Integration Demo" - t.window.icon = nil -- Icon file path - t.window.width = 800 -- Window width - t.window.height = 600 -- Window height - t.window.borderless = false -- Remove window border - t.window.resizable = false -- Allow window resizing - t.window.minwidth = 1 -- Minimum window width - t.window.minheight = 1 -- Minimum window height - t.window.fullscreen = false -- Enable fullscreen - t.window.fullscreentype = "desktop" -- Choose desktop or exclusive fullscreen - t.window.vsync = 1 -- Vertical sync mode (0=off, 1=on, -1=adaptive) - t.window.msaa = 0 -- MSAA samples - t.window.depth = nil -- Depth buffer bit depth - t.window.stencil = nil -- Stencil buffer bit depth - t.window.display = 1 -- Monitor to display window on - t.window.highdpi = false -- Enable high-dpi mode - t.window.usedpiscale = true -- Enable automatic DPI scaling - t.window.x = nil -- Window position x-coordinate - t.window.y = nil -- Window position y-coordinate - - -- Disable unused modules for faster startup and smaller memory footprint - t.modules.audio = true -- Enable audio module - t.modules.data = false -- Enable data module - t.modules.event = true -- Enable event module - t.modules.font = true -- Enable font module - t.modules.graphics = true -- Enable graphics module - t.modules.image = false -- Enable image module - t.modules.joystick = false -- Enable joystick module - t.modules.keyboard = true -- Enable keyboard module - t.modules.math = false -- Enable math module - t.modules.mouse = true -- Enable mouse module - t.modules.physics = false -- Enable physics (Box2D) module - t.modules.sound = false -- Enable sound module - t.modules.system = true -- Enable system module - t.modules.thread = true -- Enable thread module (needed for HTTP requests) - t.modules.timer = true -- Enable timer module - t.modules.touch = false -- Enable touch module - t.modules.video = false -- Enable video module - t.modules.window = true -- Enable window module -end \ No newline at end of file + t.identity = "sentry-love2d-demo" -- Save directory name + t.appendidentity = false -- Search files in source directory before save directory + t.version = "11.5" -- LÖVE version this game was made for + t.console = false -- Attach a console (Windows only) + t.accelerometerjoystick = false -- Enable accelerometer on mobile devices + t.externalstorage = false -- True to save files outside of the app folder + t.gammacorrect = false -- Enable gamma-correct rendering + + t.audio.mic = false -- Request microphone permission + t.audio.mixwithsystem = true -- Keep background music playing + + t.window.title = "Love2D Sentry Integration Demo" + t.window.icon = nil -- Icon file path + t.window.width = 800 -- Window width + t.window.height = 600 -- Window height + t.window.borderless = false -- Remove window border + t.window.resizable = false -- Allow window resizing + t.window.minwidth = 1 -- Minimum window width + t.window.minheight = 1 -- Minimum window height + t.window.fullscreen = false -- Enable fullscreen + t.window.fullscreentype = "desktop" -- Choose desktop or exclusive fullscreen + t.window.vsync = 1 -- Vertical sync mode (0=off, 1=on, -1=adaptive) + t.window.msaa = 0 -- MSAA samples + t.window.depth = nil -- Depth buffer bit depth + t.window.stencil = nil -- Stencil buffer bit depth + t.window.display = 1 -- Monitor to display window on + t.window.highdpi = false -- Enable high-dpi mode + t.window.usedpiscale = true -- Enable automatic DPI scaling + t.window.x = nil -- Window position x-coordinate + t.window.y = nil -- Window position y-coordinate + + -- Disable unused modules for faster startup and smaller memory footprint + t.modules.audio = true -- Enable audio module + t.modules.data = false -- Enable data module + t.modules.event = true -- Enable event module + t.modules.font = true -- Enable font module + t.modules.graphics = true -- Enable graphics module + t.modules.image = false -- Enable image module + t.modules.joystick = false -- Enable joystick module + t.modules.keyboard = true -- Enable keyboard module + t.modules.math = false -- Enable math module + t.modules.mouse = true -- Enable mouse module + t.modules.physics = false -- Enable physics (Box2D) module + t.modules.sound = false -- Enable sound module + t.modules.system = true -- Enable system module + t.modules.thread = true -- Enable thread module (needed for HTTP requests) + t.modules.timer = true -- Enable timer module + t.modules.touch = false -- Enable touch module + t.modules.video = false -- Enable video module + t.modules.window = true -- Enable window module +end diff --git a/examples/love2d/main.lua b/examples/love2d/main.lua index d681b0f..08cb85b 100644 --- a/examples/love2d/main.lua +++ b/examples/love2d/main.lua @@ -2,400 +2,367 @@ -- A simple app with Sentry logo and error button to demonstrate Sentry SDK -- Sentry SDK available in local sentry/ directory --- In production, copy build/sentry/ into your Love2D project +-- In production, copy src/sentry/ into your Love2D project local sentry = require("sentry") -local logger = require("sentry.logger") -- Game state local game = { - font_large = nil, - font_small = nil, - button_font = nil, - - -- Sentry logo data (simple representation) - logo_points = {}, - - -- Button state - button = { - x = 250, - y = 400, - width = 160, - height = 60, - text = "Trigger Error", - hover = false, - pressed = false - }, - - -- Fatal error button - fatal_button = { - x = 430, - y = 400, - width = 160, - height = 60, - text = "Fatal Error", - hover = false, - pressed = false - }, - - -- Error state - error_count = 0, - last_error_time = 0, - - -- Demo functions for stack trace - demo_functions = {} + font_large = nil, + font_small = nil, + button_font = nil, + + -- Sentry logo data (simple representation) + logo_points = {}, + + -- Button state + button = { + x = 250, + y = 400, + width = 160, + height = 60, + text = "Trigger Error", + hover = false, + pressed = false, + }, + + -- Fatal error button + fatal_button = { + x = 430, + y = 400, + width = 160, + height = 60, + text = "Fatal Error", + hover = false, + pressed = false, + }, + + -- Error state + error_count = 0, + last_error_time = 0, + + -- Demo functions for stack trace + demo_functions = {}, } function love.load() - -- Initialize window - love.window.setTitle("Love2D Sentry Integration Demo") - love.window.setMode(800, 600, {resizable = false}) - - -- Initialize Sentry - sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "love2d-demo", - release = "love2d-example@1.0.0", - debug = true - }) - - -- Debug: Check which transport is being used - if sentry._client and sentry._client.transport then - print("[Debug] Transport type:", type(sentry._client.transport)) - print("[Debug] Transport table:", sentry._client.transport) - else - print("[Debug] No transport found!") - end - - -- Initialize logger - logger.init({ - enable_logs = true, - max_buffer_size = 5, - flush_timeout = 2.0, - hook_print = true - }) - - -- Set user context - sentry.set_user({ - id = "love2d_user_" .. math.random(1000, 9999), - username = "love2d_demo_user" - }) - - -- Add tags for filtering - sentry.set_tag("framework", "love2d") - sentry.set_tag("version", table.concat({love.getVersion()}, ".")) - sentry.set_tag("platform", love.system.getOS()) - - -- Load fonts - game.font_large = love.graphics.newFont(32) - game.font_small = love.graphics.newFont(16) - game.button_font = love.graphics.newFont(18) - - -- Create Sentry logo points (simple S shape) - game.logo_points = { - -- Top part of S - {120, 100}, {180, 100}, {180, 130}, {150, 130}, {150, 150}, - {180, 150}, {180, 180}, {120, 180}, {120, 150}, {150, 150}, - -- Bottom part of S - {150, 170}, {120, 170}, {120, 200}, {180, 200} - } - - -- Initialize demo functions for multi-frame stack traces - game.demo_functions = { - level1 = function(user_action, error_type) - logger.info("Level 1: Processing user action %s", {user_action}) - return game.demo_functions.level2(error_type, "processing") - end, - - level2 = function(action_type, status) - logger.debug("Level 2: Executing %s with status %s", {action_type, status}) - return game.demo_functions.level3(action_type) - end, - - level3 = function(error_category) - logger.warn("Level 3: About to trigger %s error", {error_category}) - return game.demo_functions.trigger_error(error_category) - end, - - trigger_error = function(category) - game.error_count = game.error_count + 1 - game.last_error_time = love.timer.getTime() - - -- Create realistic error scenarios - if category == "button_click" then - logger.error("Critical error in button handler") - error("Love2DButtonError: Button click handler failed with code " .. math.random(1000, 9999)) - elseif category == "rendering" then - logger.error("Graphics rendering failure") - error("Love2DRenderError: Failed to render game object at frame " .. love.timer.getTime()) - else - logger.error("Generic game error occurred") - error("Love2DGameError: Unexpected game state error in category " .. tostring(category)) - end - end - } - - -- Log successful initialization - logger.info("Love2D Sentry demo initialized successfully") - logger.info("Love2D version: %s", {table.concat({love.getVersion()}, ".")}) - logger.info("Operating system: %s", {love.system.getOS()}) - - -- Add breadcrumb for debugging context - sentry.add_breadcrumb({ - message = "Love2D game initialized", - category = "game_lifecycle", - level = "info" - }) + -- Initialize window + love.window.setTitle("Love2D Sentry Integration Demo") + love.window.setMode(800, 600, { resizable = false }) + + -- Initialize Sentry + sentry.init({ + dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", + environment = "love2d-demo", + release = "love2d-example@1.0.0", + debug = true, + }) + + -- Debug: Check which transport is being used + if sentry._client and sentry._client.transport then + print("[Debug] Transport type:", type(sentry._client.transport)) + print("[Debug] Transport table:", sentry._client.transport) + else + print("[Debug] No transport found!") + end + + -- Set user context + sentry.set_user({ + id = "love2d_user_" .. math.random(1000, 9999), + username = "love2d_demo_user", + }) + + -- Add tags for filtering + sentry.set_tag("framework", "love2d") + sentry.set_tag("version", table.concat({ love.getVersion() }, ".")) + sentry.set_tag("platform", love.system.getOS()) + + -- Load fonts + game.font_large = love.graphics.newFont(32) + game.font_small = love.graphics.newFont(16) + game.button_font = love.graphics.newFont(18) + + -- Create Sentry logo points (simple S shape) + game.logo_points = { + -- Top part of S + { 120, 100 }, + { 180, 100 }, + { 180, 130 }, + { 150, 130 }, + { 150, 150 }, + { 180, 150 }, + { 180, 180 }, + { 120, 180 }, + { 120, 150 }, + { 150, 150 }, + -- Bottom part of S + { 150, 170 }, + { 120, 170 }, + { 120, 200 }, + { 180, 200 }, + } + + -- Initialize demo functions for multi-frame stack traces + game.demo_functions = { + level1 = function(user_action, error_type) + print("Level 1: Processing user action %s", { user_action }) + return game.demo_functions.level2(error_type, "processing") + end, + + level2 = function(action_type, status) + print("Level 2: Executing %s with status %s", { action_type, status }) + return game.demo_functions.level3(action_type) + end, + + level3 = function(error_category) + warn("Level 3: About to trigger %s error", { error_category }) + return game.demo_functions.trigger_error(error_category) + end, + + trigger_error = function(category) + game.error_count = game.error_count + 1 + game.last_error_time = love.timer.getTime() + + -- Create realistic error scenarios + if category == "button_click" then + error("Love2DButtonError: Button click handler failed with code " .. math.random(1000, 9999)) + elseif category == "rendering" then + error("Love2DRenderError: Failed to render game object at frame " .. love.timer.getTime()) + else + error("Love2DGameError: Unexpected game state error in category " .. tostring(category)) + end + end, + } + + -- Log successful initialization + print("Love2D Sentry demo initialized successfully") + print("Love2D version: %s", { table.concat({ love.getVersion() }, ".") }) + print("Operating system: %s", { love.system.getOS() }) + + -- Add breadcrumb for debugging context + sentry.add_breadcrumb({ + message = "Love2D game initialized", + category = "game_lifecycle", + level = "info", + }) end function love.update(dt) - -- Get mouse position for button hover detection - local mouse_x, mouse_y = love.mouse.getPosition() - local button = game.button - - -- Check if mouse is over button - local was_hover = button.hover - button.hover = (mouse_x >= button.x and mouse_x <= button.x + button.width and - mouse_y >= button.y and mouse_y <= button.y + button.height) - - -- Log hover state changes - if button.hover and not was_hover then - logger.debug("Button hover state: entered") - elseif not button.hover and was_hover then - logger.debug("Button hover state: exited") - end - - -- Flush Sentry transport periodically - sentry.flush() - - -- Flush logger periodically - if math.floor(love.timer.getTime()) % 3 == 0 then - logger.flush() - end + -- Get mouse position for button hover detection + local mouse_x, mouse_y = love.mouse.getPosition() + local button = game.button + + -- Check if mouse is over button + local was_hover = button.hover + button.hover = (mouse_x >= button.x and mouse_x <= button.x + button.width and mouse_y >= button.y and mouse_y <= button.y + button.height) + + -- Log hover state changes + if button.hover and not was_hover then + print("Button hover state: entered") + elseif not button.hover and was_hover then + print("Button hover state: exited") + end end function love.draw() - -- Clear screen with dark background - love.graphics.clear(0.1, 0.1, 0.15, 1.0) - - -- Draw title - love.graphics.setFont(game.font_large) - love.graphics.setColor(1, 1, 1, 1) - love.graphics.printf("Love2D Sentry Integration", 0, 50, love.graphics.getWidth(), "center") - - -- Draw Sentry logo (simple S shape) - love.graphics.setColor(0.4, 0.3, 0.8, 1) -- Purple color similar to Sentry - love.graphics.setLineWidth(8) - - -- Draw S shape - local logo_x, logo_y = 350, 120 - for i = 1, #game.logo_points - 1 do - local x1, y1 = game.logo_points[i][1] + logo_x, game.logo_points[i][2] + logo_y - local x2, y2 = game.logo_points[i + 1][1] + logo_x, game.logo_points[i + 1][2] + logo_y - love.graphics.line(x1, y1, x2, y2) - end - - -- Draw info text - love.graphics.setFont(game.font_small) - love.graphics.setColor(0.8, 0.8, 0.8, 1) - love.graphics.printf("This demo shows Love2D integration with Sentry SDK", 0, 280, love.graphics.getWidth(), "center") - love.graphics.printf("Red: Regular error (caught) • Purple: Fatal error (love.errorhandler)", 0, 300, love.graphics.getWidth(), "center") - love.graphics.printf("Press 'R' for regular error, 'F' for fatal error, 'ESC' to exit", 0, 320, love.graphics.getWidth(), "center") - - -- Draw button - local button = game.button - local button_color = button.hover and {0.8, 0.2, 0.2, 1} or {0.6, 0.1, 0.1, 1} - if button.pressed then - button_color = {1.0, 0.3, 0.3, 1} - end - - love.graphics.setColor(button_color[1], button_color[2], button_color[3], button_color[4]) - love.graphics.rectangle("fill", button.x, button.y, button.width, button.height, 8, 8) - - -- Draw button border - love.graphics.setColor(1, 1, 1, 0.3) - love.graphics.setLineWidth(2) - love.graphics.rectangle("line", button.x, button.y, button.width, button.height, 8, 8) - - -- Draw button text - love.graphics.setFont(game.button_font) - love.graphics.setColor(1, 1, 1, 1) - local text_width = game.button_font:getWidth(button.text) - local text_height = game.button_font:getHeight() - love.graphics.print(button.text, - button.x + (button.width - text_width) / 2, - button.y + (button.height - text_height) / 2) - - -- Draw fatal button - local fatal_button = game.fatal_button - local fatal_button_color = fatal_button.hover and {0.8, 0.2, 0.8, 1} or {0.6, 0.1, 0.6, 1} - if fatal_button.pressed then - fatal_button_color = {1.0, 0.3, 1.0, 1} - end - - love.graphics.setColor(fatal_button_color[1], fatal_button_color[2], fatal_button_color[3], fatal_button_color[4]) - love.graphics.rectangle("fill", fatal_button.x, fatal_button.y, fatal_button.width, fatal_button.height, 8, 8) - - -- Draw fatal button border - love.graphics.setColor(1, 1, 1, 0.3) - love.graphics.setLineWidth(2) - love.graphics.rectangle("line", fatal_button.x, fatal_button.y, fatal_button.width, fatal_button.height, 8, 8) - - -- Draw fatal button text - love.graphics.setFont(game.button_font) - love.graphics.setColor(1, 1, 1, 1) - local fatal_text_width = game.button_font:getWidth(fatal_button.text) - love.graphics.print(fatal_button.text, - fatal_button.x + (fatal_button.width - fatal_text_width) / 2, - fatal_button.y + (fatal_button.height - text_height) / 2) - - -- Draw stats - love.graphics.setFont(game.font_small) - love.graphics.setColor(0.7, 0.7, 0.7, 1) - love.graphics.print(string.format("Errors triggered: %d", game.error_count), 20, love.graphics.getHeight() - 60) - love.graphics.print(string.format("Framework: Love2D %s", table.concat({love.getVersion()}, ".")), 20, love.graphics.getHeight() - 40) - love.graphics.print(string.format("Platform: %s", love.system.getOS()), 20, love.graphics.getHeight() - 20) - - if game.last_error_time > 0 then - love.graphics.print(string.format("Last error: %.1fs ago", love.timer.getTime() - game.last_error_time), 400, love.graphics.getHeight() - 40) - end + -- Clear screen with dark background + love.graphics.clear(0.1, 0.1, 0.15, 1.0) + + -- Draw title + love.graphics.setFont(game.font_large) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.printf("Love2D Sentry Integration", 0, 50, love.graphics.getWidth(), "center") + + -- Draw Sentry logo (simple S shape) + love.graphics.setColor(0.4, 0.3, 0.8, 1) -- Purple color similar to Sentry + love.graphics.setLineWidth(8) + + -- Draw S shape + local logo_x, logo_y = 350, 120 + for i = 1, #game.logo_points - 1 do + local x1, y1 = game.logo_points[i][1] + logo_x, game.logo_points[i][2] + logo_y + local x2, y2 = game.logo_points[i + 1][1] + logo_x, game.logo_points[i + 1][2] + logo_y + love.graphics.line(x1, y1, x2, y2) + end + + -- Draw info text + love.graphics.setFont(game.font_small) + love.graphics.setColor(0.8, 0.8, 0.8, 1) + love.graphics.printf("This demo shows Love2D integration with Sentry SDK", 0, 280, love.graphics.getWidth(), "center") + love.graphics.printf("Red: Regular error (caught) • Purple: Fatal error (love.errorhandler)", 0, 300, love.graphics.getWidth(), "center") + love.graphics.printf("Press 'R' for regular error, 'F' for fatal error, 'ESC' to exit", 0, 320, love.graphics.getWidth(), "center") + + -- Draw button + local button = game.button + local button_color = button.hover and { 0.8, 0.2, 0.2, 1 } or { 0.6, 0.1, 0.1, 1 } + if button.pressed then button_color = { 1.0, 0.3, 0.3, 1 } end + + love.graphics.setColor(button_color[1], button_color[2], button_color[3], button_color[4]) + love.graphics.rectangle("fill", button.x, button.y, button.width, button.height, 8, 8) + + -- Draw button border + love.graphics.setColor(1, 1, 1, 0.3) + love.graphics.setLineWidth(2) + love.graphics.rectangle("line", button.x, button.y, button.width, button.height, 8, 8) + + -- Draw button text + love.graphics.setFont(game.button_font) + love.graphics.setColor(1, 1, 1, 1) + local text_width = game.button_font:getWidth(button.text) + local text_height = game.button_font:getHeight() + love.graphics.print(button.text, button.x + (button.width - text_width) / 2, button.y + (button.height - text_height) / 2) + + -- Draw fatal button + local fatal_button = game.fatal_button + local fatal_button_color = fatal_button.hover and { 0.8, 0.2, 0.8, 1 } or { 0.6, 0.1, 0.6, 1 } + if fatal_button.pressed then fatal_button_color = { 1.0, 0.3, 1.0, 1 } end + + love.graphics.setColor(fatal_button_color[1], fatal_button_color[2], fatal_button_color[3], fatal_button_color[4]) + love.graphics.rectangle("fill", fatal_button.x, fatal_button.y, fatal_button.width, fatal_button.height, 8, 8) + + -- Draw fatal button border + love.graphics.setColor(1, 1, 1, 0.3) + love.graphics.setLineWidth(2) + love.graphics.rectangle("line", fatal_button.x, fatal_button.y, fatal_button.width, fatal_button.height, 8, 8) + + -- Draw fatal button text + love.graphics.setFont(game.button_font) + love.graphics.setColor(1, 1, 1, 1) + local fatal_text_width = game.button_font:getWidth(fatal_button.text) + love.graphics.print(fatal_button.text, fatal_button.x + (fatal_button.width - fatal_text_width) / 2, fatal_button.y + (fatal_button.height - text_height) / 2) + + -- Draw stats + love.graphics.setFont(game.font_small) + love.graphics.setColor(0.7, 0.7, 0.7, 1) + love.graphics.print(string.format("Errors triggered: %d", game.error_count), 20, love.graphics.getHeight() - 60) + love.graphics.print(string.format("Framework: Love2D %s", table.concat({ love.getVersion() }, ".")), 20, love.graphics.getHeight() - 40) + love.graphics.print(string.format("Platform: %s", love.system.getOS()), 20, love.graphics.getHeight() - 20) + + if game.last_error_time > 0 then love.graphics.print(string.format("Last error: %.1fs ago", love.timer.getTime() - game.last_error_time), 400, love.graphics.getHeight() - 40) end end function love.mousepressed(x, y, button_num, istouch, presses) - if button_num == 1 then -- Left mouse button - local button = game.button - - -- Check if click is within button bounds - if x >= button.x and x <= button.x + button.width and - y >= button.y and y <= button.y + button.height then - - button.pressed = true - - -- Add breadcrumb before triggering error - sentry.add_breadcrumb({ - message = "Error button clicked", - category = "user_interaction", - level = "info", - data = { - mouse_x = x, - mouse_y = y, - error_count = game.error_count + 1 - } - }) - - -- Log the button click - logger.info("Error button clicked at position (%s, %s)", {x, y}) - logger.info("Preparing to trigger multi-frame error...") - - -- Use xpcall to capture the error with original stack trace - local function error_handler(err) - logger.error("Button click error occurred: %s", {tostring(err)}) - - sentry.capture_exception({ - type = "Love2DUserTriggeredError", - message = tostring(err) - }) - - logger.info("Error captured and sent to Sentry") - return err - end - - -- Trigger error through multi-level function calls - xpcall(function() - game.demo_functions.level1("button_click", "button_click") - end, error_handler) - - -- Check if click is within fatal button bounds - elseif x >= game.fatal_button.x and x <= game.fatal_button.x + game.fatal_button.width and - y >= game.fatal_button.y and y <= game.fatal_button.y + game.fatal_button.height then - - game.fatal_button.pressed = true - - -- Add breadcrumb before triggering fatal error - sentry.add_breadcrumb({ - message = "Fatal error button clicked - will trigger love.errorhandler", - category = "user_interaction", - level = "warning", - data = { - mouse_x = x, - mouse_y = y, - test_type = "fatal_error" - } - }) - - -- Log the fatal button click - logger.info("Fatal error button clicked at position (%s, %s)", {x, y}) - logger.info("This will trigger love.errorhandler and crash the app...") - - -- Trigger a fatal error that will go through love.errorhandler - -- This error is NOT caught with xpcall, so it will bubble up to love.errorhandler - error("Fatal Love2D error triggered by user - Testing love.errorhandler integration!") - end + if button_num == 1 then -- Left mouse button + local button = game.button + + -- Check if click is within button bounds + if x >= button.x and x <= button.x + button.width and y >= button.y and y <= button.y + button.height then + button.pressed = true + + -- Add breadcrumb before triggering error + sentry.add_breadcrumb({ + message = "Error button clicked", + category = "user_interaction", + level = "info", + data = { + mouse_x = x, + mouse_y = y, + error_count = game.error_count + 1, + }, + }) + + -- Log the button click + print("Error button clicked at position (%s, %s)", { x, y }) + print("Preparing to trigger multi-frame error...") + + -- Use xpcall to capture the error with original stack trace + local function error_handler(err) + sentry.capture_exception({ + type = "Love2DUserTriggeredError", + message = tostring(err), + }) + + print("Error captured and sent to Sentry") + return err + end + + -- Trigger error through multi-level function calls + xpcall(function() game.demo_functions.level1("button_click", "button_click") end, error_handler) + + -- Check if click is within fatal button bounds + elseif x >= game.fatal_button.x and x <= game.fatal_button.x + game.fatal_button.width and y >= game.fatal_button.y and y <= game.fatal_button.y + game.fatal_button.height then + game.fatal_button.pressed = true + + -- Add breadcrumb before triggering fatal error + sentry.add_breadcrumb({ + message = "Fatal error button clicked - will trigger love.errorhandler", + category = "user_interaction", + level = "warning", + data = { + mouse_x = x, + mouse_y = y, + test_type = "fatal_error", + }, + }) + + -- Log the fatal button click + print("Fatal error button clicked at position (%s, %s)", { x, y }) + print("This will trigger love.errorhandler and crash the app...") + + -- Trigger a fatal error that will go through love.errorhandler + -- This error is NOT caught with xpcall, so it will bubble up to love.errorhandler + error("Fatal Love2D error triggered by user - Testing love.errorhandler integration!") end + end end function love.mousereleased(x, y, button_num, istouch, presses) - if button_num == 1 then - game.button.pressed = false - game.fatal_button.pressed = false - end + if button_num == 1 then + game.button.pressed = false + game.fatal_button.pressed = false + end end function love.keypressed(key) - if key == "escape" then - -- Clean shutdown with Sentry flush - logger.info("Application shutting down") - logger.flush() - - sentry.close() - - love.event.quit() - elseif key == "r" then - -- Trigger rendering error - logger.info("Rendering error triggered via keyboard") - - sentry.add_breadcrumb({ - message = "Rendering error triggered", - category = "keyboard_interaction", - level = "info" - }) - - local function error_handler(err) - sentry.capture_exception({ - type = "Love2DRenderingError", - message = tostring(err) - }) - return err - end - - xpcall(function() - game.demo_functions.level1("render_test", "rendering") - end, error_handler) - - elseif key == "f" then - -- Trigger fatal error via keyboard - logger.info("Fatal error triggered via keyboard - will crash app") - - sentry.add_breadcrumb({ - message = "Fatal error triggered via keyboard (F key)", - category = "keyboard_interaction", - level = "warning", - data = { - test_type = "fatal_error_keyboard" - } - }) - - -- This will go through love.errorhandler and crash the app - error("Fatal Love2D error triggered by keyboard (F key) - Testing love.errorhandler integration!") + if key == "escape" then + -- Clean shutdown with Sentry flush + print("Application shutting down") + sentry.close() + + love.event.quit() + elseif key == "r" then + -- Trigger rendering error + print("Rendering error triggered via keyboard") + + sentry.add_breadcrumb({ + message = "Rendering error triggered", + category = "keyboard_interaction", + level = "info", + }) + + local function error_handler(err) + sentry.capture_exception({ + type = "Love2DRenderingError", + message = tostring(err), + }) + return err end + + xpcall(function() game.demo_functions.level1("render_test", "rendering") end, error_handler) + elseif key == "f" then + -- Trigger fatal error via keyboard + print("Fatal error triggered via keyboard - will crash app") + + sentry.add_breadcrumb({ + message = "Fatal error triggered via keyboard (F key)", + category = "keyboard_interaction", + level = "warning", + data = { + test_type = "fatal_error_keyboard", + }, + }) + + -- This will go through love.errorhandler and crash the app + error("Fatal Love2D error triggered by keyboard (F key) - Testing love.errorhandler integration!") + end end function love.quit() - -- Clean shutdown - logger.info("Love2D application quit") - logger.flush() - - sentry.close() - - return false -- Allow quit to proceed -end \ No newline at end of file + -- Clean shutdown + sentry.info("Love2D application quit") + sentry.flush() + + sentry.close() + + return false -- Allow quit to proceed +end diff --git a/examples/love2d/sentry b/examples/love2d/sentry index eae6842..c0629a9 120000 --- a/examples/love2d/sentry +++ b/examples/love2d/sentry @@ -1 +1 @@ -../../build/sentry \ No newline at end of file +../../src/sentry \ No newline at end of file diff --git a/examples/love2d/test_modules.lua b/examples/love2d/test_modules.lua deleted file mode 100644 index 7996315..0000000 --- a/examples/love2d/test_modules.lua +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env lua - --- Test script to verify Love2D example modules can be loaded - --- Add build path for modules -package.path = "../../build/?.lua;../../build/?/init.lua;" .. package.path - -print("Testing Love2D example module loading...") - --- Test basic Sentry module loading -local success, sentry = pcall(require, "sentry") -if success then - print("✓ Sentry module loaded successfully") -else - print("✗ Failed to load Sentry module:", sentry) - os.exit(1) -end - --- Test logger module -local success, logger = pcall(require, "sentry.logger") -if success then - print("✓ Logger module loaded successfully") -else - print("✗ Failed to load logger module:", logger) - os.exit(1) -end - --- Test Love2D platform modules -local success, transport = pcall(require, "sentry.platforms.love2d.transport") -if success then - print("✓ Love2D transport module loaded successfully") -else - print("✗ Failed to load Love2D transport module:", transport) - os.exit(1) -end - -local success, os_detection = pcall(require, "sentry.platforms.love2d.os_detection") -if success then - print("✓ Love2D OS detection module loaded successfully") -else - print("✗ Failed to load Love2D OS detection module:", os_detection) - os.exit(1) -end - -local success, context = pcall(require, "sentry.platforms.love2d.context") -if success then - print("✓ Love2D context module loaded successfully") -else - print("✗ Failed to load Love2D context module:", context) - os.exit(1) -end - -print("\nAll Love2D example modules loaded successfully!") -print("The Love2D example should work when run with 'love examples/love2d'") - --- Test that Love2D transport is available (without _G.love it should return false) -print("\nTesting Love2D transport availability (should be false without Love2D runtime):") -print("Love2D transport available:", transport.is_love2d_available()) - -print("\nModule loading test completed successfully!") \ No newline at end of file diff --git a/examples/nginx.lua b/examples/nginx.lua deleted file mode 100644 index fce4f85..0000000 --- a/examples/nginx.lua +++ /dev/null @@ -1,53 +0,0 @@ -local sentry = require("sentry.init") - --- Initialize Sentry for nginx/OpenResty environment -sentry.init({ - dsn = "https://your-dsn@sentry.io/project-id", - environment = "nginx", - debug = true -}) - --- Set nginx-specific context -sentry.set_tag("platform", "nginx") -sentry.set_tag("server", ngx.var.server_name or "localhost") - -sentry.set_extra("request_method", ngx.var.request_method) -sentry.set_extra("request_uri", ngx.var.request_uri) -sentry.set_extra("remote_addr", ngx.var.remote_addr) - --- Add request breadcrumb -sentry.add_breadcrumb({ - message = "HTTP request received", - category = "http", - level = "info", - data = { - method = ngx.var.request_method, - uri = ngx.var.request_uri, - user_agent = ngx.var.http_user_agent - } -}) - --- Example: Capture successful request -sentry.capture_message("Request processed successfully", "info") - --- Example: Error handling in nginx -local function process_request() - -- Simulate request processing - if not ngx.var.arg_user_id then - error("Missing required parameter: user_id") - end - - -- Process request... - ngx.say("Hello, user " .. ngx.var.arg_user_id) -end - -local success, err = pcall(process_request) -if not success then - sentry.capture_exception({ - type = "RequestError", - message = err - }) - - ngx.status = 400 - ngx.say("Bad Request") -end \ No newline at end of file diff --git a/examples/redis.lua b/examples/redis.lua deleted file mode 100644 index 06a67c6..0000000 --- a/examples/redis.lua +++ /dev/null @@ -1,39 +0,0 @@ -local sentry = require("sentry.init") -local redis_integration = require("sentry.integrations.redis") - --- Initialize Sentry with Redis transport -local RedisTransport = redis_integration.setup_redis_integration() - -sentry.init({ - dsn = "https://your-dsn@sentry.io/project-id", - environment = "redis", - transport = RedisTransport, - redis_key = "sentry:events" -}) - --- Redis-specific context -sentry.set_tag("platform", "redis") -sentry.set_extra("redis_version", "7.0") - --- Simulate Redis script execution -sentry.add_breadcrumb({ - message = "Redis script started", - category = "redis", - level = "info" -}) - --- Capture events that will be queued in Redis -sentry.capture_message("Redis script executed successfully", "info") - --- Example of error in Redis context -local success, err = pcall(function() - -- Simulate Redis operation error - redis.call("INVALID_COMMAND") -end) - -if not success then - sentry.capture_exception({ - type = "RedisError", - message = err - }) -end \ No newline at end of file diff --git a/examples/roblox/README.md b/examples/roblox/README.md index 0323341..5d03adc 100644 --- a/examples/roblox/README.md +++ b/examples/roblox/README.md @@ -6,7 +6,7 @@ Example Sentry integration for Roblox games. **Use the all-in-one file:** -1. **Copy** `sentry-all-in-one.lua` +1. **Copy** `sentry-all-in-one.luau` 2. **Paste** into ServerScriptService as a Script 3. **Update DSN** on line 18 4. **Enable HTTP**: Game Settings → Security → "Allow HTTP Requests" @@ -14,9 +14,7 @@ Example Sentry integration for Roblox games. ## 📁 Available Files -- **`sentry-all-in-one.lua`** ⭐ **Complete single-file solution** -- **`sentry-roblox-sdk.lua`** - Reusable SDK module -- **`clean-example.lua`** - Example using the SDK module +- **`sentry-all-in-one.luau`** ⭐ **Complete single-file solution** ## 🧪 Testing @@ -85,20 +83,8 @@ sentry.add_breadcrumb({ **"attempt to index nil with 'capture_message'"** → Make sure sentry.init() was called successfully first -## 🔨 Validation - -To validate the Roblox integration is ready: - -```bash -make roblox-all-in-one -# or directly: -./scripts/generate-roblox-all-in-one.sh -``` - -This checks that the `sentry-all-in-one.lua` file contains all required components and uses the standard SDK API. - ## 🎉 Ready to Go! -Use `sentry-all-in-one.lua` to get started immediately. Copy, paste, update DSN, and test! +Use `sentry-all-in-one.luau` to get started immediately. Copy, paste, update DSN, and test! **Happy debugging with Sentry! 🐛→✅** \ No newline at end of file diff --git a/examples/roblox/sentry-all-in-one.lua b/examples/roblox/sentry-all-in-one.luau similarity index 96% rename from examples/roblox/sentry-all-in-one.lua rename to examples/roblox/sentry-all-in-one.luau index 7720b9c..b965997 100644 --- a/examples/roblox/sentry-all-in-one.lua +++ b/examples/roblox/sentry-all-in-one.luau @@ -1,18 +1,18 @@ --[[ Sentry All-in-One for Roblox - + Complete Sentry integration using real SDK modules. Generated from built SDK - DO NOT EDIT MANUALLY - + To regenerate: ./scripts/generate-roblox-all-in-one.sh - + USAGE: 1. Copy this entire file - 2. Paste into ServerScriptService as a Script + 2. Paste into ServerScriptService as a Script 3. Update SENTRY_DSN below 4. Enable HTTP requests: Game Settings → Security → "Allow HTTP Requests" 5. Run the game (F5) - + API (same as other platforms): sentry.init({dsn = "your-dsn"}) sentry.capture_message("Player died!", "error") @@ -52,7 +52,7 @@ function json.encode(obj) return HttpService:JSONEncode(obj) end -function json.decode(str) +function json.decode(str) return HttpService:JSONDecode(str) end @@ -66,36 +66,36 @@ function dsn_utils.parse_dsn(dsn_string) if not dsn_string or dsn_string == "" then return {}, "DSN is required" end - + local protocol, credentials, host_path = dsn_string:match("^(https?)://([^@]+)@(.+)$") - + if not protocol or not credentials or not host_path then return {}, "Invalid DSN format" end - + -- Parse credentials (public_key or public_key:secret_key) local public_key, secret_key = credentials:match("^([^:]+):(.+)$") if not public_key then public_key = credentials secret_key = "" end - + if not public_key or public_key == "" then return {}, "Invalid DSN format" end - + -- Parse host and path local host, path = host_path:match("^([^/]+)(.*)$") if not host or not path or path == "" then return {}, "Invalid DSN format" end - + -- Extract project ID from path (last numeric segment) local project_id = path:match("/([%d]+)$") if not project_id then return {}, "Could not extract project ID from DSN" end - + return { protocol = protocol, public_key = public_key, @@ -125,7 +125,7 @@ RobloxTransport.__index = RobloxTransport function RobloxTransport:new() local transport = setmetatable({ dsn = nil, - endpoint = nil, + endpoint = nil, headers = nil }, RobloxTransport) return transport @@ -142,13 +142,13 @@ function RobloxTransport:configure(config) self.headers = { ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn), } - + -- Debug DSN configuration print("🔧 TRANSPORT CONFIGURATION DEBUG:") print(" DSN parsed successfully: " .. tostring(dsn.public_key ~= nil)) print(" Endpoint: " .. self.endpoint) print(" Headers configured: " .. tostring(self.headers ~= nil)) - + return self end @@ -157,16 +157,16 @@ function RobloxTransport:send(event) return false, "Not in Roblox environment" end - local success_service, HttpService = pcall(function() - return game:GetService("HttpService") + local success_service, _HttpService = pcall(function() + return game:GetService("HttpService") end) - if not success_service or not HttpService then + if not success_service or not _HttpService then return false, "HttpService not available in Roblox" end local body = json.encode(event) - + -- Debug output: request details print("🌐 HTTP REQUEST DEBUG:") print(" Endpoint: " .. self.endpoint) @@ -184,7 +184,7 @@ function RobloxTransport:send(event) print(" Body preview: " .. string.sub(body, 1, 100) .. "...") local success, response = pcall(function() - return HttpService:PostAsync(self.endpoint, body, + return _HttpService:PostAsync(self.endpoint, body, Enum.HttpContentType.ApplicationJson, false, self.headers) @@ -244,7 +244,7 @@ end function Scope:add_breadcrumb(breadcrumb) breadcrumb.timestamp = os.time() table.insert(self.breadcrumbs, breadcrumb) - + -- Keep only last 50 breadcrumbs if #self.breadcrumbs > 50 then table.remove(self.breadcrumbs, 1) @@ -255,7 +255,7 @@ function Scope:clone() local cloned = Scope:new() cloned.user = self.user cloned.level = self.level - + -- Deep copy tables for k, v in pairs(self.tags) do cloned.tags[k] = v @@ -266,7 +266,7 @@ function Scope:clone() for i, crumb in ipairs(self.breadcrumbs) do cloned.breadcrumbs[i] = crumb end - + return cloned end @@ -281,26 +281,26 @@ function Client:new(config) if not config.dsn then error("DSN is required") end - + local client = setmetatable({ transport = RobloxTransport:new(), scope = Scope:new(), config = config }, Client) - + client.transport:configure(config) - + print("🔧 Sentry client initialized") print(" Environment: " .. (config.environment or "production")) print(" Release: " .. (config.release or "unknown")) print(" SDK Version: " .. version()) - + return client end function Client:capture_message(message, level) level = level or "info" - + local event = { message = { message = message @@ -308,7 +308,7 @@ function Client:capture_message(message, level) level = level, timestamp = os.time(), environment = self.config.environment or "production", - release = self.config.release or "unknown", + release = self.config.release or "unknown", platform = "roblox", sdk = { name = "sentry.lua", @@ -327,19 +327,19 @@ function Client:capture_message(message, level) } } } - + print("📨 Capturing message: " .. message .. " [" .. level .. "]") print("🔄 About to call transport:send...") - + local success, result = self.transport:send(event) print("🔄 Transport call completed. Success: " .. tostring(success) .. ", Result: " .. tostring(result)) - + return success, result end function Client:capture_exception(exception, level) level = level or "error" - + local event = { exception = { values = { @@ -374,13 +374,13 @@ function Client:capture_exception(exception, level) } } } - + print("🚨 Capturing exception: " .. (exception.message or tostring(exception))) print("🔄 About to call transport:send for exception...") - + local success, result = self.transport:send(event) print("🔄 Exception transport call completed. Success: " .. tostring(success) .. ", Result: " .. tostring(result)) - + return success, result end @@ -414,7 +414,7 @@ function sentry.init(config) if not config or not config.dsn then error("Sentry DSN is required") end - + sentry._client = Client:new(config) return sentry._client end @@ -423,7 +423,7 @@ function sentry.capture_message(message, level) if not sentry._client then error("Sentry not initialized. Call sentry.init() first.") end - + return sentry._client:capture_message(message, level) end @@ -431,7 +431,7 @@ function sentry.capture_exception(exception, level) if not sentry._client then error("Sentry not initialized. Call sentry.init() first.") end - + return sentry._client:capture_exception(exception, level) end @@ -496,7 +496,7 @@ sentry.set_tag("platform", "roblox") -- Test extra context sentry.set_extra("test_type", "integration") --- Test breadcrumbs +-- Test breadcrumbs sentry.add_breadcrumb({ message = "Integration test started", category = "test", @@ -545,7 +545,7 @@ if game and game:FindFirstChild("Workspace") then workspace.SentrySDK:SetAttribute("Initialized", true) end --- Force global persistence +-- Force global persistence rawset(_G, "sentry", sentry) -- Debug global variable setup @@ -573,7 +573,7 @@ print("") print("🔹 Try these in order until one works:") print("_G.sentry.capture_message('Hello World!', 'info')") print("rawget(_G, 'sentry').capture_message('Hello rawget!', 'info')") -print("shared.sentry.capture_message('Hello shared!', 'info')") +print("shared.sentry.capture_message('Hello shared!', 'info')") print("getgenv().sentry.capture_message('Hello getgenv!', 'info')") print("") print("🔹 Exception examples:") @@ -587,7 +587,7 @@ print("_G.sentry.add_breadcrumb({message = 'Test action', category = 'test'})") print("") print("✅ Integration ready - uses real SDK " .. version() .. "!") --- Also try alternative global setups for better Roblox compatibility +-- Also try alternative global setups for better Roblox compatibility if getgenv then getgenv().sentry = sentry print("📦 Also available via getgenv().sentry") diff --git a/examples/tracing_basic.lua b/examples/tracing_basic.lua deleted file mode 100644 index fa678aa..0000000 --- a/examples/tracing_basic.lua +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env lua --- Basic distributed tracing example --- Shows core tracing concepts: transactions, spans, and event correlation - -package.path = "build/?.lua;build/?/init.lua;;" .. package.path - -local sentry = require("sentry") -local performance = require("sentry.performance") - --- Initialize Sentry with correct DSN -sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - debug = true, - environment = "tracing-basic-example" -}) - -print("🎯 Basic Distributed Tracing Example") -print("====================================") - --- Example 1: Simple transaction -print("\n📦 Example 1: Simple Transaction") -local tx1 = performance.start_transaction("user_registration", "http.server") -print("✅ Started transaction:", tx1.transaction) - --- Simulate validation -local validation_span = performance.start_span("validation.input", "Validate email and password") -print(" → Validating user input...") -os.execute("sleep 0.05") -performance.finish_span("ok") - --- Simulate database operation -local db_span = performance.start_span("db.query", "INSERT INTO users") -print(" → Creating user record...") -os.execute("sleep 0.1") -performance.finish_span("ok") - --- Capture a success event within the transaction -sentry.capture_message("User registered successfully", "info") - -performance.finish_transaction("ok") -print("✅ Transaction completed") - --- Example 2: Transaction with error -print("\n❌ Example 2: Transaction with Error") -local tx2 = performance.start_transaction("payment_processing", "task") -print("✅ Started transaction:", tx2.transaction) - --- Simulate payment validation -local validate_span = performance.start_span("payment.validate", "Validate credit card") -os.execute("sleep 0.03") -performance.finish_span("ok") - --- Simulate payment failure -local charge_span = performance.start_span("payment.charge", "Charge credit card") -os.execute("sleep 0.08") - --- Capture error within the transaction -sentry.capture_exception({ - type = "PaymentError", - message = "Card declined: insufficient funds" -}, "error") - -performance.finish_span("internal_error") -performance.finish_transaction("internal_error") -print("✅ Transaction completed (with error)") - --- Example 3: Complex nested operations -print("\n🔢 Example 3: Complex Data Pipeline") -local tx3 = performance.start_transaction("data_processing", "task") -print("✅ Started transaction:", tx3.transaction) - --- Stage 1: Data extraction -local extract_span = performance.start_span("extract.data", "Extract from external API") -print(" → Extracting data...") - --- Nested HTTP call within extraction -local api_span = performance.start_span("http.client", "GET /api/users") -os.execute("sleep 0.04") -performance.finish_span("ok") -print(" → API call completed") - -performance.finish_span("ok") -print(" → Extraction completed") - --- Stage 2: Data transformation -local transform_span = performance.start_span("transform.data", "Clean and normalize data") -print(" → Transforming data...") -os.execute("sleep 0.06") -performance.finish_span("ok") - --- Stage 3: Data loading -local load_span = performance.start_span("load.data", "Load into data warehouse") -print(" → Loading data...") -os.execute("sleep 0.05") -performance.finish_span("ok") - --- Add breadcrumb and final message -sentry.add_breadcrumb({ - message = "Data pipeline completed", - category = "processing", - level = "info", - data = { records_processed = 1250 } -}) - -sentry.capture_message("Data pipeline completed successfully", "info") - -performance.finish_transaction("ok") -print("✅ Transaction completed") - -print("\n🎉 Basic tracing examples completed!") -print("\nCheck your Sentry dashboard to see:") -print("• 3 transactions with different operations") -print("• Nested spans showing timing and hierarchy") -print("• Events correlated within transactions") -print("• Error handling within transaction context") \ No newline at end of file diff --git a/examples/tracing_client.lua b/examples/tracing_client.lua deleted file mode 100644 index 3d780ea..0000000 --- a/examples/tracing_client.lua +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env lua --- Distributed Tracing HTTP Client --- Requires: luarocks install luasocket --- Usage: First start tracing_server.lua, then run lua examples/tracing_client.lua --- This demonstrates REAL distributed tracing between processes via HTTP - -package.path = "build/?.lua;build/?/init.lua;;" .. package.path - --- Require luasocket - fail if not available -local http = require("socket.http") -local ltn12 = require("ltn12") -local sentry = require("sentry") -local tracing = require("sentry.tracing") -local performance = require("sentry.performance") - --- Initialize Sentry -sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - debug = true, - environment = "distributed-tracing-client" -}) - --- Initialize tracing -tracing.init({ - trace_propagation_targets = {"*"} -- Allow all for demo -}) - -print("🚀 Distributed Tracing Client") -print("=============================") -print("Making HTTP requests to server at http://localhost:8080") -print("Each request will propagate trace context and create distributed spans\n") - --- Helper function to make HTTP request with trace propagation -local function make_request(method, url, body, transaction) - local headers = {} - - -- Start HTTP client span FIRST - local span = transaction:start_span("http.client", method .. " " .. url) - - -- THEN get trace headers for propagation (now from the current span context) - local trace_headers = tracing.get_request_headers(url) - if trace_headers then - headers["sentry-trace"] = trace_headers["sentry-trace"] - headers["baggage"] = trace_headers["baggage"] - headers["traceparent"] = trace_headers["traceparent"] - print(" → Propagating trace headers:") - if trace_headers["sentry-trace"] then - print(" sentry-trace:", trace_headers["sentry-trace"]) - end - else - print(" → No trace headers available to propagate") - end - - -- Set content headers for POST requests - if method == "POST" and body then - headers["Content-Type"] = "application/json" - headers["Content-Length"] = tostring(#body) - end - - local response_body = {} - local result, status, response_headers - - if method == "GET" then - result, status, response_headers = http.request{ - url = url, - method = method, - headers = headers, - sink = ltn12.sink.table(response_body) - } - elseif method == "POST" then - result, status, response_headers = http.request{ - url = url, - method = method, - headers = headers, - source = ltn12.source.string(body), - sink = ltn12.sink.table(response_body) - } - end - - local response_text = table.concat(response_body) - print(" → HTTP", status, "(" .. #response_text .. " bytes)") - - -- Finish span with status - local span_status = "http_error" - if type(status) == "number" and status >= 200 and status < 300 then - span_status = "ok" - end - span:finish(span_status) - - return result, status, response_text -end - --- Demo 1: Health Check -print("📍 Demo 1: Health Check Request") --- Note: start_transaction will automatically create a new trace if none exists -local tx1 = performance.start_transaction("client_health_check", "http.client") - -local result1, status1, body1 = make_request("GET", "http://localhost:8080/", nil, tx1) -if result1 then - print(" ✅ Health check successful") - sentry.capture_message("Health check completed from client", "info") -else - print(" ❌ Health check failed:", status1) - sentry.capture_message("Health check failed from client: " .. tostring(status1), "error") -end - -tx1:finish("ok") -print() - --- Demo 2: User List Request -print("📍 Demo 2: Fetch Users") --- Start fresh trace for this demo -tracing.start_trace() -local tx2 = performance.start_transaction("client_fetch_users", "http.client") - -local result2, status2, body2 = make_request("GET", "http://localhost:8080/api/users", nil, tx2) -if result2 and status2 == 200 then - -- Parse response to show user count - local json = require("sentry.utils.json") - local success, data = pcall(json.decode, body2) - if success and data and data.users then - print(" ✅ Retrieved", #data.users, "users") - sentry.capture_message("Retrieved " .. #data.users .. " users successfully", "info") - else - print(" ✅ Users request successful") - sentry.capture_message("Users request completed", "info") - end -else - print(" ❌ Users request failed:", status2) - sentry.capture_message("Users request failed: " .. tostring(status2), "error") -end - -tx2:finish("ok") -print() - --- Demo 3: Create Order (Complex Workflow) -print("📍 Demo 3: Create Order (Complex Server Workflow)") --- Start fresh trace for this demo -tracing.start_trace() -local tx3 = performance.start_transaction("client_create_order", "http.client") - --- Simulate order data preparation -local prep_span = tx3:start_span("order.prepare", "Prepare order data") -print(" → Preparing order data...") -local order_data = { - product_id = "PROD-123", - quantity = 2, - customer_id = "CUST-456" -} -local json_body = require("sentry.utils.json").encode(order_data) -os.execute("sleep 0.02") -- Simulate prep time -prep_span:finish("ok") - -local result3, status3, body3 = make_request("POST", "http://localhost:8080/api/orders", json_body, tx3) -if result3 and status3 == 201 then - print(" ✅ Order created successfully") - sentry.capture_message("Order creation completed", "info") -else - print(" ❌ Order creation failed:", status3) - sentry.capture_message("Order creation failed: " .. tostring(status3), "error") -end - -tx3:finish("ok") -print() - --- Demo 4: Slow Request -print("📍 Demo 4: Slow Request (Performance Monitoring)") --- Start fresh trace for this demo -tracing.start_trace() -local tx4 = performance.start_transaction("client_slow_request", "http.client") - -print(" → Making slow request (will take ~800ms on server)...") -local result4, status4, body4 = make_request("GET", "http://localhost:8080/api/slow", nil, tx4) -if result4 then - print(" ✅ Slow request completed") - sentry.capture_message("Slow request completed successfully", "info") -else - print(" ❌ Slow request failed:", status4) - sentry.capture_message("Slow request failed: " .. tostring(status4), "error") -end - -tx4:finish("ok") -print() - --- Demo 5: Error Request (Error Propagation) -print("📍 Demo 5: Error Request (Distributed Error Tracing)") --- Start fresh trace for this demo -tracing.start_trace() -local tx5 = performance.start_transaction("client_error_request", "http.client") - -local result5, status5, body5 = make_request("GET", "http://localhost:8080/api/error", nil, tx5) -if result5 and status5 == 500 then - print(" ✅ Error request completed (expected 500)") - sentry.capture_message("Error endpoint tested - server error handled correctly", "info") -else - print(" ❌ Unexpected response:", status5) - sentry.capture_message("Error endpoint unexpected response: " .. tostring(status5), "warning") -end - -tx5:finish("ok") -print() - -print("🎉 Distributed tracing client demo completed!") -print() -print("Check your Sentry dashboard to see:") -print("• Client-side transactions showing HTTP requests") -print("• Server-side transactions showing request processing") -print("• Distributed traces connecting client and server spans") -print("• Error correlation across both processes") -print("• Performance data for the complete request flow") -print() -print("The traces should show:") -print(" Client Transaction") -print(" ├── http.client span (request to server)") -print(" └── Server Transaction (same trace)") -print(" ├── db.query spans") -print(" ├── cache.get spans") -print(" └── validation spans") \ No newline at end of file diff --git a/examples/tracing_server.lua b/examples/tracing_server.lua deleted file mode 100644 index 7b0ca66..0000000 --- a/examples/tracing_server.lua +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env lua --- Distributed Tracing HTTP Server --- Requires: luarocks install pegasus --- Usage: lua examples/tracing_server.lua --- Then run tracing_client.lua in another terminal - -package.path = "build/?.lua;build/?/init.lua;;" .. package.path - --- Require pegasus - fail if not available -local pegasus = require("pegasus") -local sentry = require("sentry") -local tracing = require("sentry.tracing") -local performance = require("sentry.performance") - --- Initialize Sentry -sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - debug = true, - environment = "distributed-tracing-server" -}) - --- Initialize tracing -tracing.init({ - trace_propagation_targets = {"*"} -- Allow all for demo -}) - -print("🚀 Distributed Tracing Server") -print("=============================") -print("Server starting on http://localhost:8080") -print("Endpoints:") -print(" GET / - Simple health check") -print(" GET /api/users - User list with database simulation") -print(" POST /api/orders - Order processing with complex workflow") -print(" GET /api/slow - Slow endpoint demonstrating timing") -print(" GET /api/error - Error endpoint for error tracing") -print("\nRun tracing_client.lua in another terminal to test distributed tracing") -print("Press Ctrl+C to stop\n") - --- Create server -local server = pegasus:new({ - host = "0.0.0.0", - port = 8080 -}) - --- Helper to extract trace headers -local function extract_headers(request) - local headers = {} - if request.headers then - local req_headers = request:headers() - headers["sentry-trace"] = req_headers["sentry-trace"] - headers["baggage"] = req_headers["baggage"] - headers["traceparent"] = req_headers["traceparent"] - end - return headers -end - --- Helper to handle GET / -local function handle_health_check(request, response) - local incoming_headers = extract_headers(request) - local context = tracing.continue_trace_from_request(incoming_headers) - - -- Parse incoming trace header to get correct parent span ID - local headers = require("sentry.tracing.headers") - local incoming_trace = headers.parse_sentry_trace(incoming_headers["sentry-trace"]) - - -- Start transaction with correct parent-child relationship - local tx = performance.start_transaction("GET /", "http.server", { - trace_id = context and context.trace_id, - parent_span_id = incoming_trace and incoming_trace.span_id, -- Use incoming span as parent - span_id = headers.generate_span_id() - }) - - local health = { status = "healthy", timestamp = os.time() } - - sentry.capture_message("Health check completed", "info") - tx:finish("ok") - - response:statusCode(200) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode(health)) -end - --- Helper to handle GET /api/users -local function handle_get_users(request, response) - local incoming_headers = extract_headers(request) - local context = tracing.continue_trace_from_request(incoming_headers) - - -- Parse incoming trace header to get correct parent span ID - local headers = require("sentry.tracing.headers") - local incoming_trace = headers.parse_sentry_trace(incoming_headers["sentry-trace"]) - - local tx = performance.start_transaction("GET /api/users", "http.server", { - trace_id = context and context.trace_id, - parent_span_id = incoming_trace and incoming_trace.span_id, -- Use incoming span as parent - span_id = headers.generate_span_id() - }) - print("📍 Handling GET /api/users") - - local db_span = tx:start_span("db.query", "SELECT * FROM users WHERE active = 1") - print(" → Querying database...") - os.execute("sleep 0.1") - db_span:finish("ok") - - local cache_span = tx:start_span("cache.get", "Redis GET users_list") - print(" → Checking cache...") - os.execute("sleep 0.05") - cache_span:finish("ok") - - local users = { - { id = 1, name = "Alice", email = "alice@example.com" }, - { id = 2, name = "Bob", email = "bob@example.com" }, - { id = 3, name = "Carol", email = "carol@example.com" } - } - - sentry.capture_message("Users retrieved successfully", "info") - tx:finish("ok") - - response:statusCode(200) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode({ users = users, count = #users })) -end - --- Helper to handle POST /api/orders -local function handle_create_order(request, response) - local incoming_headers = extract_headers(request) - local context = tracing.continue_trace_from_request(incoming_headers) - - -- Parse incoming trace header to get correct parent span ID - local headers = require("sentry.tracing.headers") - local incoming_trace = headers.parse_sentry_trace(incoming_headers["sentry-trace"]) - - local tx = performance.start_transaction("POST /api/orders", "http.server", { - trace_id = context and context.trace_id, - parent_span_id = incoming_trace and incoming_trace.span_id, -- Use incoming span as parent - span_id = headers.generate_span_id() - }) - print("📍 Handling POST /api/orders") - - local validation_span = tx:start_span("validation.order", "Validate order data") - print(" → Validating order...") - os.execute("sleep 0.03") - validation_span:finish("ok") - - local inventory_span = tx:start_span("inventory.check", "Check product availability") - print(" → Checking inventory...") - os.execute("sleep 0.08") - inventory_span:finish("ok") - - local payment_span = tx:start_span("payment.process", "Process payment") - print(" → Processing payment...") - os.execute("sleep 0.12") - payment_span:finish("ok") - - local create_span = tx:start_span("db.insert", "INSERT INTO orders") - print(" → Creating order record...") - os.execute("sleep 0.06") - create_span:finish("ok") - - local order = { - id = "ORD-" .. math.random(1000, 9999), - status = "confirmed", - total = 29.99 - } - - sentry.capture_message("Order created successfully: " .. order.id, "info") - tx:finish("ok") - - response:statusCode(201) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode({ order = order, message = "Order created" })) -end - --- Helper to handle GET /api/slow -local function handle_slow_endpoint(request, response) - local incoming_headers = extract_headers(request) - local context = tracing.continue_trace_from_request(incoming_headers) - - -- Parse incoming trace header to get correct parent span ID - local headers = require("sentry.tracing.headers") - local incoming_trace = headers.parse_sentry_trace(incoming_headers["sentry-trace"]) - - local tx = performance.start_transaction("GET /api/slow", "http.server", { - trace_id = context and context.trace_id, - parent_span_id = incoming_trace and incoming_trace.span_id, -- Use incoming span as parent - span_id = headers.generate_span_id() - }) - print("📍 Handling GET /api/slow") - - local external_span = tx:start_span("http.client", "External API call") - print(" → Calling external API (slow)...") - os.execute("sleep 0.5") - external_span:finish("ok") - - local slow_db_span = tx:start_span("db.query", "Complex analytical query") - print(" → Running complex query...") - os.execute("sleep 0.3") - slow_db_span:finish("ok") - - sentry.capture_message("Slow endpoint completed", "info") - tx:finish("ok") - - response:statusCode(200) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode({ - message = "Slow operation completed", - duration_ms = 800 - })) -end - --- Helper to handle GET /api/error -local function handle_error_endpoint(request, response) - local incoming_headers = extract_headers(request) - local context = tracing.continue_trace_from_request(incoming_headers) - - -- Parse incoming trace header to get correct parent span ID - local headers = require("sentry.tracing.headers") - local incoming_trace = headers.parse_sentry_trace(incoming_headers["sentry-trace"]) - - local tx = performance.start_transaction("GET /api/error", "http.server", { - trace_id = context and context.trace_id, - parent_span_id = incoming_trace and incoming_trace.span_id, -- Use incoming span as parent - span_id = headers.generate_span_id() - }) - print("📍 Handling GET /api/error") - - local work_span = tx:start_span("process.data", "Processing data") - print(" → Processing data...") - os.execute("sleep 0.05") - work_span:finish("ok") - - local error_span = tx:start_span("db.query", "Query user preferences") - print(" → Error occurred!") - - sentry.capture_exception({ - type = "DatabaseError", - message = "Connection timeout: Could not connect to database after 30s" - }, "error") - - error_span:finish("internal_error") - tx:finish("internal_error") - - response:statusCode(500) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode({ - error = "Internal server error", - message = "Database connection failed" - })) -end - --- Start server with request handler -server:start(function(request, response) - local method = request:method() - local path = request:path() - - print("📨 Incoming:", method, path) - - -- Route handling - if method == "GET" and path == "/" then - handle_health_check(request, response) - elseif method == "GET" and path == "/api/users" then - handle_get_users(request, response) - elseif method == "POST" and path == "/api/orders" then - handle_create_order(request, response) - elseif method == "GET" and path == "/api/slow" then - handle_slow_endpoint(request, response) - elseif method == "GET" and path == "/api/error" then - handle_error_endpoint(request, response) - else - -- 404 handler - response:statusCode(404) - response:addHeader("Content-Type", "application/json") - response:write(require("sentry.utils.json").encode({ - error = "Not found", - path = path, - method = method - })) - end -end) - -print("✅ Server started on http://localhost:8080") \ No newline at end of file diff --git a/examples/wrap_demo.lua b/examples/wrap_demo.lua index 4e2c06f..4d1253e 100644 --- a/examples/wrap_demo.lua +++ b/examples/wrap_demo.lua @@ -1,82 +1,77 @@ -- Demonstration of sentry.wrap() for automatic error capture -- This shows the recommended way to automatically capture unhandled errors -package.path = "build/?.lua;build/?/init.lua;" .. package.path +package.path = "src/?.lua;src/?/init.lua;" .. package.path local sentry = require("sentry.init") -- Initialize Sentry sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "demo", - release = "wrap-demo@1.0", - debug = true + dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", + environment = "demo", + release = "wrap-demo@1.0", + debug = true, }) -- Set up context that will be included with any captured errors sentry.set_user({ - id = "wrap-demo-user", - username = "error_demo" + id = "wrap-demo-user", + username = "error_demo", }) sentry.set_tag("demo_type", "wrap_function") -- Define your main application logic with parameters for stack trace visibility local function process_database_config(environment, service_name, retry_count) - local config = nil -- Simulate missing configuration - local timeout_ms = 5000 - local connection_pool_size = 10 - - -- This will cause an error that gets automatically captured - local db_url = config.database_url -- Error: attempt to index nil - return db_url .. "?timeout=" .. timeout_ms .. "&pool=" .. connection_pool_size + local config = nil -- Simulate missing configuration + local timeout_ms = 5000 + local connection_pool_size = 10 + + -- This will cause an error that gets automatically captured + local db_url = config.database_url -- Error: attempt to index nil + return db_url .. "?timeout=" .. timeout_ms .. "&pool=" .. connection_pool_size end local function validate_user_permissions(user_id, action_type, resource_id) - local permissions = {"read", "write", "admin"} - local session_data = {expires_at = os.time() + 3600, csrf_token = "abc123"} - - -- Process permissions validation - return process_database_config("production", "user_service", 3) + -- Process permissions validation + return process_database_config("production", "user_service", 3) end local function main(app_version, startup_mode) - print("=== Sentry.wrap() Demo === (v" .. app_version .. ", mode: " .. startup_mode .. ")") - local instance_id = "inst_" .. math.random(1000, 9999) - - -- Add some context - sentry.add_breadcrumb({ - message = "Application started", - category = "lifecycle", - level = "info" - }) - - print("1. Setting up user data...") - local users = {"alice", "bob", "charlie"} - - print("2. Processing payments...") - sentry.add_breadcrumb({ - message = "Starting payment processing", - category = "business", - level = "info" - }) - - -- Simulate some successful operations - for i, user in ipairs(users) do - local amount = 100 * i - print(" Processing payment for:", user, "($" .. amount .. ")") - sentry.add_breadcrumb({ - message = "Payment processed", - category = "payment", - level = "info", - data = { user = user, amount = amount } - }) - end - - print("3. Validating user permissions...") - -- This will ultimately cause an error through the call chain - local result = validate_user_permissions("user_12345", "database_write", "config_table") - - print("This line should never be reached:", result) + print("=== Sentry.wrap() Demo === (v" .. app_version .. ", mode: " .. startup_mode .. ")") + -- Add some context + sentry.add_breadcrumb({ + message = "Application started", + category = "lifecycle", + level = "info", + }) + + print("1. Setting up user data...") + local users = { "alice", "bob", "charlie" } + + print("2. Processing payments...") + sentry.add_breadcrumb({ + message = "Starting payment processing", + category = "business", + level = "info", + }) + + -- Simulate some successful operations + for i, user in ipairs(users) do + local amount = 100 * i + print(" Processing payment for:", user, "($" .. amount .. ")") + sentry.add_breadcrumb({ + message = "Payment processed", + category = "payment", + level = "info", + data = { user = user, amount = amount }, + }) + end + + print("3. Validating user permissions...") + -- This will ultimately cause an error through the call chain + local result = validate_user_permissions("user_12345", "database_write", "config_table") + + print("This line should never be reached:", result) end -- Method 1: Simple wrap - Errors terminate the program but get sent to Sentry @@ -84,69 +79,61 @@ print("\n=== Method 1: Simple Wrap ===") local success, result = sentry.wrap(function() main("2.1.4", "production") end) if success then - print("✓ Program completed successfully") + print("✓ Program completed successfully") else - print("✗ Program failed but error was captured in Sentry") - print("Error:", result) + print("✗ Program failed but error was captured in Sentry") + print("Error:", result) end print("\n=== Method 2: Custom Error Handler ===") -- Method 2: Custom error handler - You can handle errors gracefully local function attempt_risky_operation(operation_id, max_retries, timeout_seconds) - local cache_key = "op_" .. operation_id - local start_time = os.time() - - print("Attempting risky operation (ID: " .. operation_id .. ", retries: " .. max_retries .. ")...") - - local risky_data = nil - return risky_data.missing_field .. " cached as " .. cache_key -- This will error -end + local cache_key = "op_" .. operation_id + print("Attempting risky operation (ID: " .. operation_id .. ", retries: " .. max_retries .. ")...") -local function main_with_recovery() - return attempt_risky_operation("op_789", 5, 30) + local risky_data = nil + return risky_data.missing_field .. " cached as " .. cache_key -- This will error end +local function main_with_recovery() return attempt_risky_operation("op_789", 5, 30) end + local function custom_error_handler(err) - local error_id = "err_" .. math.random(10000, 99999) - local timestamp = os.date("%Y-%m-%d %H:%M:%S") - - print("Custom handler: Caught error [" .. error_id .. " at " .. timestamp .. "]:", err) - print("Custom handler: Performing cleanup...") - -- You could do cleanup, logging, etc. here - return "Handled gracefully (error ID: " .. error_id .. ")" + local error_id = "err_" .. math.random(10000, 99999) + local timestamp = os.date("%Y-%m-%d %H:%M:%S") + + print("Custom handler: Caught error [" .. error_id .. " at " .. timestamp .. "]:", err) + print("Custom handler: Performing cleanup...") + -- You could do cleanup, logging, etc. here + return "Handled gracefully (error ID: " .. error_id .. ")" end local success2, result2 = sentry.wrap(main_with_recovery, custom_error_handler) if success2 then - print("✓ Program handled error gracefully") - print("Result:", result2) + print("✓ Program handled error gracefully") + print("Result:", result2) else - print("✗ Even custom handler couldn't save us") - print("Error:", result2) + print("✗ Even custom handler couldn't save us") + print("Error:", result2) end print("\n=== Comparison with Manual Approach ===") -- Show equivalent manual approach for comparison local function manual_error_simulation(task_name, priority_level) - local task_id = "task_" .. math.random(100, 999) - local worker_id = "worker_A1" - - error("Manual error handling for " .. task_name .. " (priority: " .. priority_level .. ", task: " .. task_id .. ")") + local task_id = "task_" .. math.random(100, 999) + error("Manual error handling for " .. task_name .. " (priority: " .. priority_level .. ", task: " .. task_id .. ")") end local function manual_approach() - return xpcall(function() - manual_error_simulation("data_processing", "high") - end, function(err) - sentry.capture_exception({ - type = "ManualError", - message = tostring(err) - }) - return err - end) + return xpcall(function() manual_error_simulation("data_processing", "high") end, function(err) + sentry.capture_exception({ + type = "ManualError", + message = tostring(err), + }) + return err + end) end local manual_success, manual_result = manual_approach() @@ -156,6 +143,6 @@ sentry.close() print("\n=== Summary ===") print("• sentry.wrap(main_function) - Simple automatic error capture") -print("• sentry.wrap(main_function, error_handler) - Custom error handling") +print("• sentry.wrap(main_function, error_handler) - Custom error handling") print("• All Sentry context (user, tags, breadcrumbs) is automatically included") -print("• Much simpler than manually wrapping every error-prone operation") \ No newline at end of file +print("• Much simpler than manually wrapping every error-prone operation") diff --git a/platforms/lua/http_client.lua b/platforms/lua/http_client.lua deleted file mode 100644 index 7677dac..0000000 --- a/platforms/lua/http_client.lua +++ /dev/null @@ -1,205 +0,0 @@ ----@class platforms.lua.http_client ---- HTTP client integrations for standard Lua with popular HTTP libraries ---- Provides automatic trace header injection for outgoing requests - -local http_client = {} - -local tracing = require("sentry.tracing") - ----LuaSocket HTTP client integration ----@class LuaSocketIntegration -local luasocket = {} - ----Wrap LuaSocket http.request function with tracing ----@param http_module table The LuaSocket http module ----@return table wrapped_module Wrapped http module with tracing -function luasocket.wrap_http_module(http_module) - if not http_module or not http_module.request then - error("Invalid LuaSocket http module - missing request function") - end - - local original_request = http_module.request - - -- Wrap the request function - http_module.request = function(url_or_options, body) - local url, options - - -- Handle both forms: request(url, body) and request(options_table) - if type(url_or_options) == "string" then - url = url_or_options - options = { url = url, source = body } - else - options = url_or_options or {} - url = options.url - end - - -- Add trace headers - if url and tracing.is_active() then - options.headers = options.headers or {} - local trace_headers = tracing.get_request_headers(url) - for key, value in pairs(trace_headers) do - options.headers[key] = value - end - end - - -- Call original request function - if type(url_or_options) == "string" then - return original_request(options, body) - else - return original_request(options) - end - end - - return http_module -end - ----lua-http client integration ----@class LuaHttpIntegration -local lua_http = {} - ----Wrap lua-http request object with tracing ----@param http_request table The lua-http request object ----@return table wrapped_request Wrapped request object with tracing -function lua_http.wrap_request(http_request) - if not http_request or not http_request.get_headers_as_sequence then - error("Invalid lua-http request object") - end - - -- Store original go method - local original_go = http_request.go - - http_request.go = function(self, ...) - -- Add trace headers before sending - if tracing.is_active() then - local url = self:get_uri() - local trace_headers = tracing.get_request_headers(url) - - for key, value in pairs(trace_headers) do - self:append_header(key, value) - end - end - - return original_go(self, ...) - end - - return http_request -end - ----Generic HTTP client wrapper that works with function-based clients ----@param http_client_func function HTTP client function (url, options) -> response ----@return function wrapped_client Wrapped HTTP client with tracing -function http_client.wrap_generic_client(http_client_func) - return function(url, options) - return tracing.wrap_http_request(http_client_func, url, options) - end -end - ----LuaSocket integration functions -http_client.luasocket = luasocket - ----lua-http integration functions -http_client.lua_http = lua_http - ----Auto-detection and wrapping of common HTTP clients ----@param http_module table The HTTP module to wrap ----@return table wrapped_module Wrapped HTTP module -function http_client.auto_wrap(http_module) - if not http_module then - return http_module - end - - -- Detect LuaSocket http module - if http_module.request and type(http_module.request) == "function" then - return luasocket.wrap_http_module(http_module) - end - - -- Detect lua-http request object - if http_module.get_headers_as_sequence and http_module.go then - return lua_http.wrap_request(http_module) - end - - -- Return unwrapped if not recognized - return http_module -end - ----Create a simple HTTP GET function with tracing support ----@param http_lib string? HTTP library to use ("luasocket", "lua-http", or auto-detect) ----@return function get_func HTTP GET function with tracing -function http_client.create_get_function(http_lib) - http_lib = http_lib or "luasocket" - - if http_lib == "luasocket" then - local http = require("socket.http") - local wrapped_http = luasocket.wrap_http_module(http) - - return function(url, headers) - local options = { - url = url, - headers = headers or {} - } - local body, status, response_headers = wrapped_http.request(options) - return { - body = body, - status = status, - headers = response_headers - } - end - end - - error("Unsupported HTTP library: " .. tostring(http_lib)) -end - ----Create a traced HTTP request function that can be used with any HTTP library ----@param make_request function Function that takes (url, options) and returns response ----@return function traced_request Function with automatic trace header injection -function http_client.create_traced_request(make_request) - if type(make_request) ~= "function" then - error("make_request must be a function") - end - - return function(url, options) - return tracing.wrap_http_request(make_request, url, options) - end -end - ----Middleware function for adding tracing to any HTTP client ----@param client_function function The original HTTP client function ----@param extract_url function? Optional function to extract URL from arguments ----@return function middleware_function Wrapped function with tracing -function http_client.create_middleware(client_function, extract_url) - if type(client_function) ~= "function" then - error("client_function must be a function") - end - - extract_url = extract_url or function(args) return args[1] end - - return function(...) - local args = {...} - local url = extract_url(args) - - if url and tracing.is_active() then - -- Modify headers in the arguments - -- This is library-specific and would need customization - local trace_headers = tracing.get_request_headers(url) - - -- Try to find headers in arguments - -- Check if first argument has headers (config object pattern) - if args[1] and type(args[1]) == "table" and args[1].headers then - args[1].headers = args[1].headers or {} - for key, value in pairs(trace_headers) do - args[1].headers[key] = value - end - -- Check if second argument contains headers (url, options pattern) - elseif args[2] and type(args[2]) == "table" then - args[2].headers = args[2].headers or {} - for key, value in pairs(trace_headers) do - args[2].headers[key] = value - end - end - end - - return client_function((table.unpack or unpack)(args)) - end -end - -return http_client \ No newline at end of file diff --git a/platforms/lua/http_server.lua b/platforms/lua/http_server.lua deleted file mode 100644 index 1bcefdf..0000000 --- a/platforms/lua/http_server.lua +++ /dev/null @@ -1,273 +0,0 @@ ----@class platforms.lua.http_server ---- HTTP server integrations for standard Lua with popular HTTP server libraries ---- Provides automatic trace continuation from incoming request headers - -local http_server = {} - -local tracing = require("sentry.tracing") - ----Pegasus.lua server integration ----@class PegasusIntegration -local pegasus = {} - ----Wrap Pegasus server start method with tracing ----@param pegasus_server table The Pegasus server instance ----@return table wrapped_server Wrapped server instance with tracing -function pegasus.wrap_server(pegasus_server) - if not pegasus_server or not pegasus_server.start then - error("Invalid Pegasus server - missing start method") - end - - local original_start = pegasus_server.start - - pegasus_server.start = function(self, handler_or_callback) - local wrapped_handler - - if type(handler_or_callback) == "function" then - -- Wrap the callback function - wrapped_handler = function(request, response) - -- Extract headers from Pegasus request object - local request_headers = {} - - if request and request.headers then - -- Pegasus stores headers in request.headers() function - if type(request.headers) == "function" then - local headers = request:headers() - if headers then - for key, value in pairs(headers) do - request_headers[key:lower()] = value - end - end - elseif type(request.headers) == "table" then - -- Fallback for other servers that use table - for key, value in pairs(request.headers) do - request_headers[key:lower()] = value - end - end - end - - -- Continue trace from request - tracing.continue_trace_from_request(request_headers) - - -- Call original handler - local success, result = pcall(handler_or_callback, request, response) - - if not success then - error(result) - end - - return result - end - else - wrapped_handler = handler_or_callback - end - - return original_start(self, wrapped_handler) - end - - return pegasus_server -end - ----Create a Pegasus middleware function for tracing ----@return function middleware_function Pegasus middleware that continues traces -function pegasus.create_middleware() - return function(request, response, next) - -- Extract headers - local request_headers = {} - if request and request.headers then - for key, value in pairs(request.headers) do - request_headers[key:lower()] = value - end - end - - -- Continue trace - tracing.continue_trace_from_request(request_headers) - - -- Call next middleware/handler - if next then - return next() - end - end -end - ----lua-http server integration ----@class LuaHttpServerIntegration -local lua_http_server = {} - ----Wrap lua-http server listen method with tracing ----@param server table The lua-http server instance ----@return table wrapped_server Wrapped server with tracing -function lua_http_server.wrap_server(server) - if not server or not server.listen then - error("Invalid lua-http server - missing listen method") - end - - local original_listen = server.listen - - server.listen = function(self, handler) - local wrapped_handler = function(stream) - -- lua-http provides headers via stream:get_headers() - local headers_sequence = stream:get_headers() - local request_headers = {} - - if headers_sequence then - -- Convert headers sequence to table - for name, value in headers_sequence:each() do - request_headers[name:lower()] = value - end - end - - -- Continue trace - tracing.continue_trace_from_request(request_headers) - - -- Call original handler - return handler(stream) - end - - return original_listen(self, wrapped_handler) - end - - return server -end - ----OpenResty/nginx-lua integration (for environments that support it) ----@class OpenRestyIntegration -local openresty = {} - ----Wrap OpenResty handler with tracing ----@param handler function The OpenResty request handler ----@return function wrapped_handler Handler that continues traces -function openresty.wrap_handler(handler) - return function(...) - -- Extract headers from nginx request - local request_headers = {} - - -- Check if we're in OpenResty environment - if ngx and ngx.req and ngx.req.get_headers then - local headers = ngx.req.get_headers() - for key, value in pairs(headers) do - request_headers[key:lower()] = value - end - end - - -- Continue trace - tracing.continue_trace_from_request(request_headers) - - -- Call original handler - return handler(...) - end -end - ----Generic HTTP server middleware creator ----@param extract_headers function Function that extracts headers from request object ----@return function middleware Middleware function for the specific server -function http_server.create_generic_middleware(extract_headers) - if type(extract_headers) ~= "function" then - error("extract_headers must be a function") - end - - return function(request, response, next) - local request_headers = extract_headers(request) - - -- Normalize headers to lowercase keys - local normalized_headers = {} - if request_headers then - for key, value in pairs(request_headers) do - if type(key) == "string" then - normalized_headers[key:lower()] = value - end - end - end - - -- Continue trace - tracing.continue_trace_from_request(normalized_headers) - - -- Call next if provided (middleware pattern) - if next and type(next) == "function" then - return next() - end - end -end - ----Wrap any server handler function with tracing ----@param handler function The original handler function ----@param extract_headers function Function to extract headers from request ----@return function wrapped_handler Handler with tracing support -function http_server.wrap_handler(handler, extract_headers) - if type(handler) ~= "function" then - error("handler must be a function") - end - - extract_headers = extract_headers or function(request) - -- Default header extraction (works with many libraries) - if request and request.headers then - return request.headers - end - return {} - end - - return function(request, response, ...) - local request_headers = extract_headers(request) - - -- Normalize headers - local normalized_headers = {} - if request_headers then - for key, value in pairs(request_headers) do - if type(key) == "string" then - normalized_headers[key:lower()] = value - end - end - end - - -- Continue trace - tracing.continue_trace_from_request(normalized_headers) - - -- Call original handler - return handler(request, response, ...) - end -end - ----Auto-detect and wrap server objects ----@param server table The server object to wrap ----@return table wrapped_server Wrapped server with tracing -function http_server.auto_wrap(server) - if not server then - return server - end - - -- Detect Pegasus server - if server.start and server.location then - return pegasus.wrap_server(server) - end - - -- Detect lua-http server - if server.listen and server.bind then - return lua_http_server.wrap_server(server) - end - - -- Return unwrapped if not recognized - return server -end - ----Create a simple HTTP server with tracing support ----@param server_type string Server type ("pegasus", "lua-http") ----@param config table Server configuration ----@return table server Configured server with tracing -function http_server.create_server(server_type, config) - config = config or {} - - if server_type == "pegasus" then - local pegasus_lib = require("pegasus") - local server = pegasus_lib:new(config) - return pegasus.wrap_server(server) - end - - error("Unsupported server type: " .. tostring(server_type)) -end - --- Export integration modules -http_server.pegasus = pegasus -http_server.lua_http_server = lua_http_server -http_server.openresty = openresty - -return http_server \ No newline at end of file diff --git a/platforms/lua/init.lua b/platforms/lua/init.lua deleted file mode 100644 index 2aaf949..0000000 --- a/platforms/lua/init.lua +++ /dev/null @@ -1,274 +0,0 @@ ----@class platforms.lua ---- Standard Lua and LuaJIT platform implementation for distributed tracing ---- Provides initialization and auto-detection of HTTP libraries - -local platform = {} - -local tracing = require("sentry.tracing") -local http_client = require("platforms.lua.http_client") -local http_server = require("platforms.lua.http_server") - ----Platform configuration -platform.config = { - name = "lua", - version = _VERSION or "Unknown", - auto_instrument = true, - supported_libraries = { - http_client = {"socket.http", "http.request"}, - http_server = {"pegasus", "http.server"} - } -} - ----Auto-instrumentation state -local auto_instrumentation_enabled = false -local instrumented_modules = {} - ----Initialize platform-specific tracing features ----@param options table? Platform initialization options -function platform.init(options) - options = options or {} - - -- Initialize core tracing - tracing.init(options.tracing) - - -- Enable auto-instrumentation if requested - if options.auto_instrument ~= false then - platform.enable_auto_instrumentation() - end - - -- Store platform-specific options - platform._options = options -end - ----Enable automatic instrumentation of HTTP libraries -function platform.enable_auto_instrumentation() - if auto_instrumentation_enabled then - return - end - - auto_instrumentation_enabled = true - - -- Auto-instrument HTTP client libraries - platform.auto_instrument_http_clients() - - -- Auto-instrument HTTP server libraries - platform.auto_instrument_http_servers() -end - ----Disable automatic instrumentation -function platform.disable_auto_instrumentation() - auto_instrumentation_enabled = false - - -- Restore original modules if they were instrumented - for module_name, original_module in pairs(instrumented_modules) do - package.loaded[module_name] = original_module - end - - instrumented_modules = {} -end - ----Auto-instrument HTTP client libraries -function platform.auto_instrument_http_clients() - -- Instrument LuaSocket HTTP module when required - local original_require = require - - require = function(module_name) - local module = original_require(module_name) - - -- Instrument socket.http when loaded - if module_name == "socket.http" and module and module.request then - if not instrumented_modules[module_name] then - instrumented_modules[module_name] = module - module = http_client.luasocket.wrap_http_module(module) - package.loaded[module_name] = module - end - end - - -- Instrument http.request (lua-http) when loaded - if module_name == "http.request" and module and module.new then - if not instrumented_modules[module_name] then - instrumented_modules[module_name] = module - - -- Wrap the constructor to return wrapped request objects - local original_new = module.new - module.new = function(...) - local request = original_new(...) - return http_client.lua_http.wrap_request(request) - end - - package.loaded[module_name] = module - end - end - - return module - end -end - ----Auto-instrument HTTP server libraries -function platform.auto_instrument_http_servers() - local original_require = require - - require = function(module_name) - local module = original_require(module_name) - - -- Instrument Pegasus when loaded - if module_name == "pegasus" and module and module.new then - if not instrumented_modules[module_name] then - instrumented_modules[module_name] = module - - -- Wrap the constructor to return wrapped server objects - local original_new = module.new - module.new = function(...) - local server = original_new(...) - return http_server.pegasus.wrap_server(server) - end - - package.loaded[module_name] = module - end - end - - return module - end -end - ----Manually instrument an HTTP client ----@param client table The HTTP client object or module ----@param client_type string? Type of client ("luasocket", "lua-http", or auto-detect) ----@return table instrumented_client Instrumented client -function platform.instrument_http_client(client, client_type) - if not client then - error("HTTP client is required") - end - - client_type = client_type or "auto" - - if client_type == "auto" then - return http_client.auto_wrap(client) - elseif client_type == "luasocket" then - return http_client.luasocket.wrap_http_module(client) - elseif client_type == "lua-http" then - return http_client.lua_http.wrap_request(client) - else - error("Unsupported HTTP client type: " .. tostring(client_type)) - end -end - ----Manually instrument an HTTP server ----@param server table The HTTP server object ----@param server_type string? Type of server ("pegasus", "lua-http", or auto-detect) ----@return table instrumented_server Instrumented server -function platform.instrument_http_server(server, server_type) - if not server then - error("HTTP server is required") - end - - server_type = server_type or "auto" - - if server_type == "auto" then - return http_server.auto_wrap(server) - elseif server_type == "pegasus" then - return http_server.pegasus.wrap_server(server) - elseif server_type == "lua-http" then - return http_server.lua_http_server.wrap_server(server) - else - error("Unsupported HTTP server type: " .. tostring(server_type)) - end -end - ----Create a traced HTTP client function ----@param client_type string? Type of client to create ("luasocket") ----@return function http_get HTTP GET function with tracing -function platform.create_http_client(client_type) - client_type = client_type or "luasocket" - - return http_client.create_get_function(client_type) -end - ----Create a traced HTTP server ----@param server_type string Type of server to create ("pegasus") ----@param config table? Server configuration ----@return table server HTTP server with tracing -function platform.create_http_server(server_type, config) - return http_server.create_server(server_type, config) -end - ----Wrap any function to continue traces from HTTP headers ----@param handler function The handler function to wrap ----@param extract_headers function? Custom header extraction function ----@return function wrapped_handler Handler with trace continuation -function platform.wrap_request_handler(handler, extract_headers) - return http_server.wrap_handler(handler, extract_headers) -end - ----Create middleware for popular frameworks ----@param framework string Framework type ("pegasus", "generic") ----@return function middleware Middleware function -function platform.create_middleware(framework) - if framework == "pegasus" then - return http_server.pegasus.create_middleware() - elseif framework == "generic" then - return http_server.create_generic_middleware(function(request) - return request.headers or {} - end) - else - error("Unsupported framework: " .. tostring(framework)) - end -end - ----Get platform information ----@return table info Platform information -function platform.get_info() - return { - name = platform.config.name, - version = platform.config.version, - auto_instrumentation_enabled = auto_instrumentation_enabled, - instrumented_modules = (function() - local keys = {} - for k, _ in pairs(instrumented_modules) do - table.insert(keys, k) - end - return keys - end)(), - tracing_active = tracing.is_active(), - supported_libraries = platform.config.supported_libraries - } -end - ----Check if a specific HTTP library is available ----@param library_name string Name of the library to check ----@return boolean available True if library is available -function platform.is_library_available(library_name) - local success, _ = pcall(require, library_name) - return success -end - ----Get recommended HTTP client for this platform ----@return string|nil client_type Recommended HTTP client type or nil if none available -function platform.get_recommended_http_client() - if platform.is_library_available("socket.http") then - return "luasocket" - elseif platform.is_library_available("http.request") then - return "lua-http" - end - - return nil -end - ----Get recommended HTTP server for this platform ----@return string|nil server_type Recommended HTTP server type or nil if none available -function platform.get_recommended_http_server() - if platform.is_library_available("pegasus") then - return "pegasus" - elseif platform.is_library_available("http.server") then - return "lua-http" - end - - return nil -end - --- Export core functionality for convenience -platform.tracing = tracing -platform.http_client = http_client -platform.http_server = http_server - -return platform \ No newline at end of file diff --git a/platforms/noop/init.lua b/platforms/noop/init.lua deleted file mode 100644 index 0b3d257..0000000 --- a/platforms/noop/init.lua +++ /dev/null @@ -1,162 +0,0 @@ ----@class platforms.noop ---- No-op platform implementation for unsupported platforms ---- Provides stub implementations that do nothing but don't error - -local noop = {} - ----Initialize no-op platform (does nothing) ----@param options table? Ignored options -function noop.init(options) - -- Do nothing - tracing not supported on this platform -end - ----No-op enable auto-instrumentation -function noop.enable_auto_instrumentation() - -- Do nothing -end - ----No-op disable auto-instrumentation -function noop.disable_auto_instrumentation() - -- Do nothing -end - ----No-op HTTP client instrumentation ----@param client table The HTTP client (returned unchanged) ----@param client_type string? Ignored ----@return table client Unchanged client -function noop.instrument_http_client(client, client_type) - return client -end - ----No-op HTTP server instrumentation ----@param server table The HTTP server (returned unchanged) ----@param server_type string? Ignored ----@return table server Unchanged server -function noop.instrument_http_server(server, server_type) - return server -end - ----No-op HTTP client creation ----@param client_type string? Ignored ----@return function noop_client Function that returns empty table -function noop.create_http_client(client_type) - return function(url, options) - return {} - end -end - ----No-op HTTP server creation ----@param server_type string Ignored ----@param config table? Ignored ----@return table noop_server Empty server object -function noop.create_http_server(server_type, config) - return {} -end - ----No-op request handler wrapper ----@param handler function The handler (returned unchanged) ----@param extract_headers function? Ignored ----@return function handler Unchanged handler -function noop.wrap_request_handler(handler, extract_headers) - return handler -end - ----No-op middleware creation ----@param framework string Ignored ----@return function noop_middleware Middleware that does nothing -function noop.create_middleware(framework) - return function(request, response, next) - if next then - return next() - end - end -end - ----Get platform information ----@return table info Platform information indicating no-op mode -function noop.get_info() - return { - name = "noop", - version = "1.0.0", - auto_instrumentation_enabled = false, - instrumented_modules = {}, - tracing_active = false, - supported_libraries = {}, - reason = "Tracing not supported on this platform" - } -end - ----Check if library is available (always false for no-op) ----@param library_name string Ignored ----@return boolean available Always false -function noop.is_library_available(library_name) - return false -end - ----Get recommended HTTP client (always nil for no-op) ----@return string|nil client_type Always nil -function noop.get_recommended_http_client() - return nil -end - ----Get recommended HTTP server (always nil for no-op) ----@return string|nil server_type Always nil -function noop.get_recommended_http_server() - return nil -end - --- Stub tracing module that does nothing -local noop_tracing = { - init = function() end, - continue_trace_from_request = function() return {} end, - get_request_headers = function() return {} end, - start_trace = function() return {} end, - create_child = function() return {} end, - get_current_trace_info = function() return nil end, - is_active = function() return false end, - clear = function() end, - attach_trace_context_to_event = function(event) return event end, - wrap_http_request = function(client, url, options) return client(url, options) end, - wrap_http_handler = function(handler) return handler end, - generate_ids = function() return {trace_id = "", span_id = ""} end, - get_envelope_trace_header = function() return nil end -} - --- Stub HTTP modules -local noop_http_client = { - wrap_generic_client = function(client) return client end, - luasocket = { - wrap_http_module = function(module) return module end - }, - lua_http = { - wrap_request = function(request) return request end - }, - auto_wrap = function(module) return module end, - create_get_function = function() return function() return {} end end, - create_traced_request = function(make_request) return make_request end, - create_middleware = function(client) return client end -} - -local noop_http_server = { - wrap_handler = function(handler) return handler end, - create_generic_middleware = function() return function() end end, - pegasus = { - wrap_server = function(server) return server end, - create_middleware = function() return function() end end - }, - lua_http_server = { - wrap_server = function(server) return server end - }, - openresty = { - wrap_handler = function(handler) return handler end - }, - auto_wrap = function(server) return server end, - create_server = function() return {} end -} - --- Export stub functionality -noop.tracing = noop_tracing -noop.http_client = noop_http_client -noop.http_server = noop_http_server - -return noop \ No newline at end of file diff --git a/roblox.json b/roblox.json index 5dd2df3..90efb8e 100644 --- a/roblox.json +++ b/roblox.json @@ -1,6 +1,6 @@ { "name": "sentry", - "version": "0.0.6", + "version": "0.0.7", "description": "Sentry SDK for Roblox - Error tracking and monitoring", "author": "bruno-garcia", "license": "MIT", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..d7cbae3 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,66 @@ +# Development Scripts + +This directory contains development automation scripts for the Sentry Lua SDK. + +## dev.lua + +A cross-platform Lua script that replaces traditional Makefile functionality, ensuring compatibility across Windows, macOS, and Linux. + +### Usage + +```bash +# Show help +lua scripts/dev.lua help + +# Install all dependencies +lua scripts/dev.lua install + +# Run tests +lua scripts/dev.lua test + +# Run tests with coverage (generates luacov.report.out) +lua scripts/dev.lua coverage + +# Run linter +lua scripts/dev.lua lint + +# Check code formatting +lua scripts/dev.lua format-check + +# Format code +lua scripts/dev.lua format + +# Test rockspec installation +lua scripts/dev.lua test-rockspec + +# Clean build artifacts +lua scripts/dev.lua clean + +# Run full CI pipeline +lua scripts/dev.lua ci +``` + +### Requirements + +- Lua (5.1+) +- LuaRocks +- For formatting: StyLua (`cargo install stylua`) + +### Cross-Platform Commands + +The script automatically detects the platform and uses appropriate commands: + +- **Windows**: Uses `dir`, `rmdir`, `xcopy` +- **Unix/Linux/macOS**: Uses `ls`, `rm`, `cp` + +This ensures the same script works across all development environments without modification. + +### CI Integration + +The `ci` command runs the complete pipeline: +1. Linting with luacheck +2. Format checking with StyLua +3. Test suite with busted +4. Coverage reporting with luacov + +This matches what runs in GitHub Actions, allowing developers to run the same checks locally. \ No newline at end of file diff --git a/scripts/bump-version.ps1 b/scripts/bump-version.ps1 index 0f64f2c..81849b9 100644 --- a/scripts/bump-version.ps1 +++ b/scripts/bump-version.ps1 @@ -30,9 +30,6 @@ function Replace-TextInFile { $repoRoot = "$PSScriptRoot/.." -# Update tealdoc.yml -Replace-TextInFile "$repoRoot/tealdoc.yml" '(?<=project_version: ").*?(?=")' $newVersion - # Update roblox.json Replace-TextInFile "$repoRoot/roblox.json" '(?<="version": ").*?(?=")' $newVersion @@ -63,10 +60,9 @@ if ($rockspec) { } # Update centralized version file -Replace-TextInFile "$repoRoot/src/sentry/version.tl" '(?<=VERSION = ").*?(?=")' $newVersion +Replace-TextInFile "$repoRoot/src/sentry/version.lua" '(?<=VERSION = ").*?(?=")' $newVersion # Update test spec files Replace-TextInFile "$repoRoot/spec/sentry_spec.lua" '(?<=sentry\.set_tag\("version", ").*?(?=")' $newVersion Write-Host "Version bump completed successfully to $newVersion" -Write-Host "Please run 'make build' to rebuild the Lua files from Teal sources" \ No newline at end of file diff --git a/scripts/dev.lua b/scripts/dev.lua new file mode 100755 index 0000000..db976d0 --- /dev/null +++ b/scripts/dev.lua @@ -0,0 +1,277 @@ +#!/usr/bin/env lua + +local function run_command(cmd, description) + if description then print("🔧 " .. description) end + print("$ " .. cmd) + local result = os.execute(cmd) + if result ~= 0 and result ~= true then + print("❌ Command failed: " .. cmd) + os.exit(1) + end + print("✅ Success") + print("") + return result +end + +local function file_exists(path) + local file = io.open(path, "r") + if file then + file:close() + return true + end + return false +end + +local function get_luarocks_path() + -- Try to detect luarocks installation + local handle = io.popen("luarocks path 2>/dev/null || echo ''") + local result = handle:read("*a") + handle:close() + return result and result ~= "" +end + +local function install_dependencies() + print("📦 Installing dependencies...") + + if not get_luarocks_path() then + print("❌ LuaRocks not found. Please install LuaRocks first.") + os.exit(1) + end + + -- Install test dependencies + run_command("luarocks install busted", "Installing busted test framework") + run_command("luarocks install lua-cjson", "Installing JSON support") + run_command("luarocks install luasocket", "Installing socket support") + + -- Install luasec with OpenSSL directory if available + local openssl_dir = os.getenv("OPENSSL_DIR") + if openssl_dir then + run_command("luarocks install luasec OPENSSL_DIR=" .. openssl_dir, "Installing SSL/HTTPS support") + else + run_command("luarocks install luasec", "Installing SSL/HTTPS support") + end + + run_command("luarocks install luacov", "Installing coverage tool") + run_command("luarocks install luacov-reporter-lcov", "Installing LCOV reporter") + + -- Install development dependencies + run_command("luarocks install luacheck", "Installing luacheck linter") +end + +local function run_tests() + print("🧪 Running tests...") + + if not file_exists("spec") then + print("❌ No spec directory found") + os.exit(1) + end + + -- Run busted tests + run_command("busted", "Running test suite") +end + +local function run_coverage() + print("📊 Running tests with coverage...") + + -- Run tests with coverage + run_command("busted --coverage", "Running tests with coverage") + + -- Generate coverage reports + if file_exists("luacov.stats.out") then + run_command("luacov", "Generating coverage report") + if file_exists("luacov.report.out") then + print("✅ Coverage report generated: luacov.report.out") + end + else + print("⚠️ No coverage stats found. Make sure busted --coverage ran successfully.") + end +end + +local function run_lint() + print("🔍 Running linter...") + + -- Try to find luacheck in common locations + local luacheck_paths = { + "luacheck", + "~/.luarocks/bin/luacheck", + "/usr/local/bin/luacheck", + os.getenv("HOME") .. "/.luarocks/bin/luacheck", + } + + local luacheck_cmd = nil + for _, path in ipairs(luacheck_paths) do + local expanded_path = path:gsub("~", os.getenv("HOME") or "~") + local test_result = os.execute(expanded_path .. " --version 2>/dev/null") + if test_result == 0 or test_result == true then + luacheck_cmd = expanded_path + break + end + end + + if not luacheck_cmd then + print("❌ luacheck not found. Install with: luarocks install luacheck") + print(" Or ensure luacheck is in your PATH") + os.exit(1) + end + + -- Run luacheck + run_command(luacheck_cmd .. " .", "Running luacheck") +end + +local function run_format_check() + print("✨ Checking code formatting...") + + -- Check if stylua is available + local handle = io.popen("stylua --version 2>/dev/null") + local stylua_version = handle:read("*l") + handle:close() + + if not stylua_version then + print("⚠️ StyLua not found. Install with: cargo install stylua") + return + end + + -- Check formatting + local result = os.execute("stylua --check .") + if result ~= 0 and result ~= true then + print("❌ Code formatting issues found. Run: lua scripts/dev.lua format") + os.exit(1) + else + print("✅ Code formatting is correct") + end +end + +local function run_format() + print("✨ Formatting code...") + + -- Check if stylua is available + local handle = io.popen("stylua --version 2>/dev/null") + local stylua_version = handle:read("*l") + handle:close() + + if not stylua_version then + print("❌ StyLua not found. Install with: cargo install stylua") + os.exit(1) + end + + -- Format code + run_command("stylua .", "Formatting Lua code") +end + +local function test_love2d() + print("TODO") + +end + +local function test_rockspec() + print("📋 Testing rockspec installation...") + + -- Find rockspec file + local handle = io.popen("ls *.rockspec 2>/dev/null || dir *.rockspec 2>nul") + local rockspec = handle:read("*l") + handle:close() + + if not rockspec then + print("❌ No rockspec file found") + os.exit(1) + end + + -- Try local installation first (safer for CI environments), fallback to system + local success = false + local luarocks_cmd = "luarocks make --local " .. rockspec + local result = os.execute(luarocks_cmd) + + if result == 0 or result == true then + print("✅ Rockspec test passed (local installation)") + success = true + else + -- Fallback to system installation + luarocks_cmd = "luarocks make " .. rockspec + result = os.execute(luarocks_cmd) + if result == 0 or result == true then + print("✅ Rockspec test passed (system installation)") + success = true + end + end + + if not success then + print("❌ Rockspec test failed") + os.exit(1) + end +end + +local function clean() + print("🧹 Cleaning build artifacts...") + + -- Clean coverage files + if file_exists("luacov.stats.out") then run_command("rm luacov.stats.out", "Removing coverage stats") end + if file_exists("luacov.report.out") then run_command("rm luacov.report.out", "Removing coverage report") end + if file_exists("coverage.info") then run_command("rm coverage.info", "Removing LCOV report") end + if file_exists("test-results.xml") then run_command("rm test-results.xml", "Removing test results") end +end + +local function show_help() + print("🚀 Sentry Lua SDK Development Script") + print("") + print("Usage: lua scripts/dev.lua [command]") + print("") + print("Commands:") + print(" install Install dependencies") + print(" test Run tests") + print(" coverage Run tests with coverage") + print(" lint Run linter (luacheck)") + print(" format-check Check code formatting") + print(" format Format code with StyLua") + print(" test-rockspec Test rockspec installation") + print(" ci-love2d Run Love2D integration tests") + print(" clean Clean artifacts") + print(" ci Run full CI pipeline (lint, format-check, test, coverage)") + print(" help Show this help") + print("") + print("Examples:") + print(" lua scripts/dev.lua install") + print(" lua scripts/dev.lua test") + print(" lua scripts/dev.lua ci-love2d") + print(" lua scripts/dev.lua ci") +end + +local function run_ci() + print("🚀 Running full CI pipeline...") + run_lint() + run_format_check() + run_tests() + run_coverage() + print("🎉 CI pipeline completed successfully!") +end + +-- Main execution +local command = arg and arg[1] or "help" + +if command == "install" then + install_dependencies() +elseif command == "test" then + run_tests() +elseif command == "coverage" then + run_coverage() +elseif command == "lint" then + run_lint() +elseif command == "format-check" then + run_format_check() +elseif command == "format" then + run_format() +elseif command == "test-rockspec" then + test_rockspec() +elseif command == "ci-love2d" then + test_love2d() +elseif command == "clean" then + clean() +elseif command == "ci" then + run_ci() +elseif command == "help" then + show_help() +else + print("❌ Unknown command: " .. command) + print("") + show_help() + os.exit(1) +end diff --git a/scripts/generate-roblox-all-in-one.sh b/scripts/generate-roblox-all-in-one.sh deleted file mode 100755 index d09d6af..0000000 --- a/scripts/generate-roblox-all-in-one.sh +++ /dev/null @@ -1,586 +0,0 @@ -#!/bin/bash -# -# Generate Roblox All-in-One Integration -# -# This script assembles a complete Roblox integration from the real SDK modules -# built from src/ (after Teal compilation). This ensures the example uses the -# actual SDK code and stays updated with SDK changes. -# -# Usage: ./scripts/generate-roblox-all-in-one.sh -# - -set -e - -echo "🔨 Generating Roblox All-in-One Integration from Real SDK" -echo "=======================================================" - -OUTPUT_FILE="examples/roblox/sentry-all-in-one.lua" - -# Check if SDK is built -if [ ! -f "build/sentry/init.lua" ]; then - echo "❌ SDK not built. Run 'make build' first." - exit 1 -fi - -echo "✅ Found built SDK" - -# Read required SDK modules -echo "📖 Reading SDK modules..." - -read_module() { - local file="$1" - if [ -f "$file" ]; then - echo "✅ Reading: $file" - cat "$file" - else - echo "❌ Missing: $file" - exit 1 - fi -} - -# Create the all-in-one file by combining real SDK modules -cat > "$OUTPUT_FILE" << 'HEADER_EOF' ---[[ - Sentry All-in-One for Roblox - - Complete Sentry integration using real SDK modules. - Generated from built SDK - DO NOT EDIT MANUALLY - - To regenerate: ./scripts/generate-roblox-all-in-one.sh - - USAGE: - 1. Copy this entire file - 2. Paste into ServerScriptService as a Script - 3. Update SENTRY_DSN below - 4. Enable HTTP requests: Game Settings → Security → "Allow HTTP Requests" - 5. Run the game (F5) - - API (same as other platforms): - sentry.init({dsn = "your-dsn"}) - sentry.capture_message("Player died!", "error") - sentry.capture_exception({type = "GameError", message = "Boss fight failed"}) - sentry.set_user({id = tostring(player.UserId), username = player.Name}) - sentry.set_tag("level", "10") - sentry.add_breadcrumb({message = "Player entered dungeon", category = "navigation"}) -]]-- - --- ⚠️ UPDATE THIS WITH YOUR SENTRY DSN -local SENTRY_DSN = "https://your-key@your-org.ingest.sentry.io/your-project-id" - -print("🚀 Starting Sentry All-in-One Integration") -print("DSN: ***" .. string.sub(SENTRY_DSN, -10)) -print("=" .. string.rep("=", 40)) - --- Embedded SDK Modules (from real build/) --- This ensures we use the actual SDK code with proper version info - -HEADER_EOF - -# Add version module -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- VERSION MODULE (from build/sentry/version.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -# Read version and create local version -VERSION=$(grep -o '"[^"]*"' build/sentry/version.lua | tr -d '"') -cat >> "$OUTPUT_FILE" << VERSION_EOF -local function version() - return "$VERSION" -end -VERSION_EOF - -# Add JSON utils -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- JSON UTILS (from build/sentry/utils/json.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -# For Roblox, we use HttpService for JSON, so create a simple wrapper -cat >> "$OUTPUT_FILE" << 'JSON_EOF' -local json = {} -local HttpService = game:GetService("HttpService") - -function json.encode(obj) - return HttpService:JSONEncode(obj) -end - -function json.decode(str) - return HttpService:JSONDecode(str) -end -JSON_EOF - -# Add DSN utils (extract the core functions we need) -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- DSN UTILS (adapted from build/sentry/utils/dsn.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -cat >> "$OUTPUT_FILE" << 'DSN_EOF' -local dsn_utils = {} - -function dsn_utils.parse_dsn(dsn_string) - if not dsn_string or dsn_string == "" then - return {}, "DSN is required" - end - - local protocol, credentials, host_path = dsn_string:match("^(https?)://([^@]+)@(.+)$") - - if not protocol or not credentials or not host_path then - return {}, "Invalid DSN format" - end - - -- Parse credentials (public_key or public_key:secret_key) - local public_key, secret_key = credentials:match("^([^:]+):(.+)$") - if not public_key then - public_key = credentials - secret_key = "" - end - - if not public_key or public_key == "" then - return {}, "Invalid DSN format" - end - - -- Parse host and path - local host, path = host_path:match("^([^/]+)(.*)$") - if not host or not path or path == "" then - return {}, "Invalid DSN format" - end - - -- Extract project ID from path (last numeric segment) - local project_id = path:match("/([%d]+)$") - if not project_id then - return {}, "Could not extract project ID from DSN" - end - - return { - protocol = protocol, - public_key = public_key, - secret_key = secret_key or "", - host = host, - path = path, - project_id = project_id - }, nil -end - -function dsn_utils.build_ingest_url(dsn) - return "https://" .. dsn.host .. "/api/" .. dsn.project_id .. "/store/" -end - -function dsn_utils.build_auth_header(dsn) - return string.format("Sentry sentry_version=7, sentry_key=%s, sentry_client=sentry-lua/%s", - dsn.public_key, version()) -end -DSN_EOF - -# Add Roblox Transport (from the real built module) -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- ROBLOX TRANSPORT (from build/sentry/platforms/roblox/transport.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -# Extract the core transport logic and adapt for standalone use -cat >> "$OUTPUT_FILE" << 'TRANSPORT_EOF' -local RobloxTransport = {} -RobloxTransport.__index = RobloxTransport - -function RobloxTransport:new() - local transport = setmetatable({ - dsn = nil, - endpoint = nil, - headers = nil - }, RobloxTransport) - return transport -end - -function RobloxTransport:configure(config) - local dsn, err = dsn_utils.parse_dsn(config.dsn or "") - if err then - error("Invalid DSN: " .. err) - end - - self.dsn = dsn - self.endpoint = dsn_utils.build_ingest_url(dsn) - self.headers = { - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn), - } - - -- Configuration successful - - return self -end - -function RobloxTransport:send(event) - if not game then - return false, "Not in Roblox environment" - end - - local success_service, HttpService = pcall(function() - return game:GetService("HttpService") - end) - - if not success_service or not HttpService then - return false, "HttpService not available in Roblox" - end - - local body = json.encode(event) - - local success, response = pcall(function() - return HttpService:PostAsync(self.endpoint, body, - Enum.HttpContentType.ApplicationJson, - false, - self.headers) - end) - - if success then - return true, "Event sent via Roblox HttpService" - else - return false, "Roblox HTTP error: " .. tostring(response) - end -end -TRANSPORT_EOF - -# Add Scope (simplified from the real SDK) -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- SCOPE (from build/sentry/core/scope.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -cat >> "$OUTPUT_FILE" << 'SCOPE_EOF' -local Scope = {} -Scope.__index = Scope - -function Scope:new() - return setmetatable({ - user = nil, - tags = {}, - extra = {}, - breadcrumbs = {}, - level = nil - }, Scope) -end - -function Scope:set_user(user) - self.user = user -end - -function Scope:set_tag(key, value) - self.tags[key] = tostring(value) -end - -function Scope:set_extra(key, value) - self.extra[key] = value -end - -function Scope:add_breadcrumb(breadcrumb) - breadcrumb.timestamp = os.time() - table.insert(self.breadcrumbs, breadcrumb) - - -- Keep only last 50 breadcrumbs - if #self.breadcrumbs > 50 then - table.remove(self.breadcrumbs, 1) - end -end - -function Scope:clone() - local cloned = Scope:new() - cloned.user = self.user - cloned.level = self.level - - -- Deep copy tables - for k, v in pairs(self.tags) do - cloned.tags[k] = v - end - for k, v in pairs(self.extra) do - cloned.extra[k] = v - end - for i, crumb in ipairs(self.breadcrumbs) do - cloned.breadcrumbs[i] = crumb - end - - return cloned -end -SCOPE_EOF - -# Add Client (adapted from real SDK) -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- CLIENT (from build/sentry/core/client.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -cat >> "$OUTPUT_FILE" << 'CLIENT_EOF' -local Client = {} -Client.__index = Client - -function Client:new(config) - if not config.dsn then - error("DSN is required") - end - - local client = setmetatable({ - transport = RobloxTransport:new(), - scope = Scope:new(), - config = config - }, Client) - - client.transport:configure(config) - - -- Client initialized successfully - - return client -end - -function Client:capture_message(message, level) - level = level or "info" - - local event = { - message = { - message = message - }, - level = level, - timestamp = os.time(), - environment = self.config.environment or "production", - release = self.config.release or "unknown", - platform = "roblox", - sdk = { - name = "sentry.lua", - version = version() - }, - server_name = "roblox-server", - user = self.scope.user, - tags = self.scope.tags, - extra = self.scope.extra, - breadcrumbs = self.scope.breadcrumbs, - contexts = { - roblox = { - version = version(), - place_id = tostring(game.PlaceId), - job_id = game.JobId or "unknown" - } - } - } - - return self.transport:send(event) -end - -function Client:capture_exception(exception, level) - level = level or "error" - - local event = { - exception = { - values = { - { - type = exception.type or "RobloxError", - value = exception.message or tostring(exception) - } - } - }, - level = level, - timestamp = os.time(), - environment = self.config.environment or "production", - release = self.config.release or "unknown", - platform = "roblox", - sdk = { - name = "sentry.lua", - version = version() - }, - server_name = "roblox-server", - user = self.scope.user, - tags = self.scope.tags, - extra = self.scope.extra, - breadcrumbs = self.scope.breadcrumbs, - contexts = { - roblox = { - version = version(), - place_id = tostring(game.PlaceId), - job_id = game.JobId or "unknown" - } - } - } - - return self.transport:send(event) -end - -function Client:set_user(user) - self.scope:set_user(user) -end - -function Client:set_tag(key, value) - self.scope:set_tag(key, value) -end - -function Client:set_extra(key, value) - self.scope:set_extra(key, value) -end - -function Client:add_breadcrumb(breadcrumb) - self.scope:add_breadcrumb(breadcrumb) -end -CLIENT_EOF - -# Add main Sentry API (from real SDK) -echo "" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "-- MAIN SENTRY API (from build/sentry/init.lua)" >> "$OUTPUT_FILE" -echo "-- ============================================================================" >> "$OUTPUT_FILE" -echo "" >> "$OUTPUT_FILE" - -cat >> "$OUTPUT_FILE" << 'SENTRY_EOF' -local sentry = {} - -function sentry.init(config) - if not config or not config.dsn then - error("Sentry DSN is required") - end - - sentry._client = Client:new(config) - return sentry._client -end - -function sentry.capture_message(message, level) - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - return sentry._client:capture_message(message, level) -end - -function sentry.capture_exception(exception, level) - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - return sentry._client:capture_exception(exception, level) -end - -function sentry.set_user(user) - if sentry._client then - sentry._client:set_user(user) - end -end - -function sentry.set_tag(key, value) - if sentry._client then - sentry._client:set_tag(key, value) - end -end - -function sentry.set_extra(key, value) - if sentry._client then - sentry._client:set_extra(key, value) - end -end - -function sentry.add_breadcrumb(breadcrumb) - if sentry._client then - sentry._client:add_breadcrumb(breadcrumb) - end -end - -function sentry.flush() - -- No-op for Roblox (HTTP is immediate) -end - -function sentry.close() - if sentry._client then - sentry._client = nil - end -end -SENTRY_EOF - -# Add initialization and test code -cat >> "$OUTPUT_FILE" << 'INIT_EOF' - --- Initialize Sentry with provided DSN -sentry.init({ - dsn = SENTRY_DSN, - environment = "roblox-production", - release = "1.0.0" -}) - --- Run integration tests -sentry.capture_message("Sentry integration test", "info") -sentry.capture_exception({ - type = "IntegrationTestError", - message = "Test exception from Roblox all-in-one integration" -}) - --- Make sentry available globally for easy access with multiple methods -_G.sentry = sentry - --- Also store in shared (if available) -if shared then - shared.sentry = sentry -end - --- Store in getgenv if available (common in executors) -if getgenv then - getgenv().sentry = sentry -end - --- Store in game.ReplicatedStorage for cross-script access -if game and game:GetService("ReplicatedStorage") then - local replicatedStorage = game:GetService("ReplicatedStorage") - if not replicatedStorage:FindFirstChild("SentrySDK") then - local sentryValue = Instance.new("ObjectValue") - sentryValue.Name = "SentrySDK" - sentryValue.Parent = replicatedStorage - sentryValue:SetAttribute("Initialized", true) - end -end - --- Store in workspace as well for fallback -if game and game:FindFirstChild("Workspace") then - local workspace = game.Workspace - if not workspace:FindFirstChild("SentrySDK") then - local sentryObject = Instance.new("ObjectValue") - sentryObject.Name = "SentrySDK" - sentryObject.Parent = workspace - end - -- Store actual reference in a persistent way - workspace.SentrySDK:SetAttribute("Initialized", true) -end - --- Force global persistence -rawset(_G, "sentry", sentry) - --- Sentry SDK is now available globally - -print("✅ Sentry integration ready - SDK " .. version()) -print("💡 Use: _G.sentry.capture_message('Hello', 'info')") - --- Also try alternative global setups for better Roblox compatibility -if getgenv then - getgenv().sentry = sentry -end - --- Set up a test function that can be called easily -_G.testSentry = function() - _G.sentry.capture_message("Test from _G.testSentry() function", "info") - print("✅ Test message sent!") -end -INIT_EOF - -echo "✅ Generated $OUTPUT_FILE" - -# Get file size -FILE_SIZE=$(wc -c < "$OUTPUT_FILE") -FILE_SIZE_KB=$((FILE_SIZE / 1024)) -echo "📊 File size: ${FILE_SIZE_KB} KB" -echo "📦 SDK version: $VERSION" - -echo "" -echo "🎉 Generation completed successfully!" -echo "" -echo "📋 The all-in-one file is ready for use:" -echo " • Uses real SDK modules from build/" -echo " • Proper SDK version: $VERSION" -echo " • Standard API: sentry.capture_message(), sentry.set_tag(), etc." -echo " • Copy $OUTPUT_FILE into Roblox Studio" -echo " • Update the SENTRY_DSN variable and test" \ No newline at end of file diff --git a/sentry-0.0.6-1.rockspec b/sentry-0.0.6-1.rockspec deleted file mode 100644 index e484092..0000000 --- a/sentry-0.0.6-1.rockspec +++ /dev/null @@ -1,74 +0,0 @@ -rockspec_format = "3.0" -package = "sentry" -version = "0.0.6-1" -source = { - url = "git+https://github.com/getsentry/sentry-lua.git", - tag = "0.0.6" -} -description = { - summary = "Platform-agnostic Sentry SDK for Lua", - detailed = [[ - A comprehensive Sentry SDK for Lua environments with distributed tracing, - structured logging, and cross-platform support. Written in Teal Language - for type safety and compiled to Lua during installation. - ]], - homepage = "https://github.com/getsentry/sentry-lua", - license = "MIT" -} -dependencies = { - "lua >= 5.1", - "lua-cjson", - "luasocket" -} -build_dependencies = { - "tl" -} -build = { - type = "command", - build_command = [[ - echo "=== Starting Teal compilation ===" - - # Create build directory structure - mkdir -p build/sentry/core - mkdir -p build/sentry/logger - mkdir -p build/sentry/performance - mkdir -p build/sentry/platforms/defold - mkdir -p build/sentry/platforms/love2d - mkdir -p build/sentry/platforms/nginx - mkdir -p build/sentry/platforms/redis - mkdir -p build/sentry/platforms/roblox - mkdir -p build/sentry/platforms/standard - mkdir -p build/sentry/platforms/test - mkdir -p build/sentry/tracing - mkdir -p build/sentry/utils - - # Compile all Teal files to Lua - find src/sentry -name "*.tl" -type f | while read -r tl_file; do - lua_file=$(echo "$tl_file" | sed 's|^src/|build/|' | sed 's|\.tl$|.lua|') - echo "Compiling: $tl_file -> $lua_file" - tl gen "$tl_file" -o "$lua_file" - if [ $? -ne 0 ]; then - echo "ERROR: Failed to compile $tl_file" - exit 1 - fi - done - - echo "=== Teal compilation completed ===" - ]], - install_command = [[ - echo "=== Installing compiled Lua files ===" - - # Create target directory - mkdir -p "$(LUADIR)/sentry" - - # Copy all compiled Lua files preserving directory structure - cd build && find sentry -name "*.lua" -type f | while read -r lua_file; do - target_dir=$(dirname "$(LUADIR)/$lua_file") - mkdir -p "$target_dir" - cp "$lua_file" "$(LUADIR)/$lua_file" - echo "Installed: $lua_file -> $(LUADIR)/$lua_file" - done - - echo "=== Installation completed ===" - ]] -} \ No newline at end of file diff --git a/sentry-0.0.7-1.rockspec b/sentry-0.0.7-1.rockspec new file mode 100644 index 0000000..b13611a --- /dev/null +++ b/sentry-0.0.7-1.rockspec @@ -0,0 +1,29 @@ +rockspec_format = "3.0" +package = "sentry" +version = "0.0.7-1" +source = { + url = "git+https://github.com/getsentry/sentry-lua.git", + tag = "0.0.7" +} +description = { + summary = "Sentry SDK for Lua", + detailed = [[ + A Sentry SDK for Lua focus on portability + ]], + homepage = "https://github.com/getsentry/sentry-lua", + license = "MIT" +} +dependencies = { + "lua >= 5.1", + "luasocket >= 3.0", + "luasec >= 1.0", +} +test_dependencies = { + "luacheck >= 0.23.0", +} +build = { + type = "builtin", + modules = { + ["sentry"] = "src/sentry/init.lua", + } +} \ No newline at end of file diff --git a/spec/baggage_headers_spec.lua b/spec/baggage_headers_spec.lua deleted file mode 100644 index bbaeb57..0000000 --- a/spec/baggage_headers_spec.lua +++ /dev/null @@ -1,190 +0,0 @@ --- Tests for baggage header parsing and generation --- Based on W3C Baggage specification and Sentry usage - -local headers = require("sentry.tracing.headers") - -describe("baggage header", function() - describe("parsing", function() - it("should parse single key-value pair", function() - local header = "sentry-environment=production" - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("production", result["sentry-environment"]) - end) - - it("should parse multiple key-value pairs", function() - local header = "sentry-environment=production,sentry-release=1.0.0,user-id=123" - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("production", result["sentry-environment"]) - assert.are.equal("1.0.0", result["sentry-release"]) - assert.are.equal("123", result["user-id"]) - end) - - it("should handle whitespace around key-value pairs", function() - local header = " sentry-environment=production , sentry-release=1.0.0 " - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("production", result["sentry-environment"]) - assert.are.equal("1.0.0", result["sentry-release"]) - end) - - it("should ignore properties after semicolon", function() - local header = "sentry-environment=production;properties=ignored,sentry-release=1.0.0" - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("production", result["sentry-environment"]) - assert.are.equal("1.0.0", result["sentry-release"]) - end) - - it("should handle empty values", function() - local header = "sentry-environment=,sentry-release=1.0.0" - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("", result["sentry-environment"]) - assert.are.equal("1.0.0", result["sentry-release"]) - end) - - it("should skip malformed entries", function() - local header = "sentry-environment=production,malformed,sentry-release=1.0.0" - local result = headers.parse_baggage(header) - - assert.is_not_nil(result) - assert.are.equal("production", result["sentry-environment"]) - assert.are.equal("1.0.0", result["sentry-release"]) - assert.is_nil(result["malformed"]) - end) - - it("should handle nil input", function() - local result = headers.parse_baggage(nil) - assert.is_not_nil(result) - - -- Count the number of keys in the table - local count = 0 - for _ in pairs(result) do - count = count + 1 - end - assert.are.equal(0, count) - end) - - it("should handle empty string", function() - local result = headers.parse_baggage("") - assert.is_not_nil(result) - assert.are.equal("table", type(result)) - end) - - it("should handle non-string input", function() - local result = headers.parse_baggage(123) - assert.is_not_nil(result) - assert.are.equal("table", type(result)) - end) - end) - - describe("generation", function() - it("should generate baggage header for single entry", function() - local baggage = { - ["sentry-environment"] = "production" - } - - local result = headers.generate_baggage(baggage) - assert.are.equal("sentry-environment=production", result) - end) - - it("should generate baggage header for multiple entries", function() - local baggage = { - ["sentry-environment"] = "production", - ["sentry-release"] = "1.0.0" - } - - local result = headers.generate_baggage(baggage) - assert.is_not_nil(result) - assert.is_not_nil(result:match("sentry%-environment=production")) - assert.is_not_nil(result:match("sentry%-release=1%.0%.0")) - assert.is_not_nil(result:match(",")) - end) - - it("should URL encode special characters", function() - local baggage = { - ["test-key"] = "value,with;special=characters%" - } - - local result = headers.generate_baggage(baggage) - assert.is_not_nil(result) - assert.is_not_nil(result:match("%%2C")) -- encoded comma - assert.is_not_nil(result:match("%%3B")) -- encoded semicolon - assert.is_not_nil(result:match("%%3D")) -- encoded equals - assert.is_not_nil(result:match("%%25")) -- encoded percent - end) - - it("should handle empty values", function() - local baggage = { - ["empty-key"] = "" - } - - local result = headers.generate_baggage(baggage) - assert.are.equal("empty-key=", result) - end) - - it("should return nil for empty baggage", function() - local baggage = {} - local result = headers.generate_baggage(baggage) - assert.is_nil(result) - end) - - it("should return nil for nil input", function() - local result = headers.generate_baggage(nil) - assert.is_nil(result) - end) - - it("should return nil for non-table input", function() - local result = headers.generate_baggage("invalid") - assert.is_nil(result) - end) - - it("should skip non-string keys and values", function() - local baggage = { - ["valid-key"] = "valid-value", - [123] = "invalid-key", - ["invalid-value"] = 456 - } - - local result = headers.generate_baggage(baggage) - assert.are.equal("valid-key=valid-value", result) - end) - end) - - describe("round-trip consistency", function() - it("should parse what it generates", function() - local original = { - ["sentry-environment"] = "production", - ["sentry-release"] = "1.0.0", - ["user-id"] = "123" - } - - local header = headers.generate_baggage(original) - local parsed = headers.parse_baggage(header) - - assert.are.equal(original["sentry-environment"], parsed["sentry-environment"]) - assert.are.equal(original["sentry-release"], parsed["sentry-release"]) - assert.are.equal(original["user-id"], parsed["user-id"]) - end) - - it("should handle special characters in round-trip", function() - local original = { - ["test-key"] = "value,with;special=characters%" - } - - local header = headers.generate_baggage(original) - local parsed = headers.parse_baggage(header) - - -- Note: Basic implementation may not handle URL decoding perfectly - -- This test documents expected behavior - assert.is_not_nil(parsed["test-key"]) - end) - end) -end) \ No newline at end of file diff --git a/spec/context_filtering_spec.lua b/spec/context_filtering_spec.lua deleted file mode 100644 index 461978e..0000000 --- a/spec/context_filtering_spec.lua +++ /dev/null @@ -1,159 +0,0 @@ -describe("Context Filtering", function() - local Client - local Scope - local os_utils - local original_get_os_info - - before_each(function() - Client = require("sentry.core.client") - Scope = require("sentry.core.scope") - os_utils = require("sentry.utils.os") - - -- Mock get_os_info function - original_get_os_info = os_utils.get_os_info - end) - - after_each(function() - -- Restore original function - if original_get_os_info then - os_utils.get_os_info = original_get_os_info - end - end) - - describe("OS context with nil version", function() - it("should exclude version field when nil", function() - -- Mock OS detection to return nil version - os_utils.get_os_info = function() - return {name = "TestOS", version = nil} - end - - local client = Client:new({ - dsn = "https://test@sentry.io/123456", - test_transport = true - }) - - -- Check that OS context was set correctly - assert.is_not_nil(client.scope) - assert.is_not_nil(client.scope.contexts.os) - assert.are.equal("TestOS", client.scope.contexts.os.name) - assert.is_nil(client.scope.contexts.os.version) - end) - - it("should include version field when present", function() - -- Mock OS detection to return valid version - os_utils.get_os_info = function() - return {name = "macOS", version = "15.5"} - end - - local client = Client:new({ - dsn = "https://test@sentry.io/123456", - test_transport = true - }) - - -- Check that OS context includes version - assert.are.equal("macOS", client.scope.contexts.os.name) - assert.are.equal("15.5", client.scope.contexts.os.version) - end) - - it("should not set OS context when detection fails", function() - -- Mock OS detection to return nil - os_utils.get_os_info = function() - return nil - end - - local client = Client:new({ - dsn = "https://test@sentry.io/123456", - test_transport = true - }) - - -- Check that no OS context was set - assert.is_nil(client.scope.contexts.os) - end) - end) - - describe("Scope set_context", function() - local scope - - before_each(function() - scope = Scope:new() - end) - - it("should store context with all fields", function() - scope:set_context("test", { - name = "TestContext", - version = "1.0", - description = "Test description" - }) - - assert.are.equal("TestContext", scope.contexts.test.name) - assert.are.equal("1.0", scope.contexts.test.version) - assert.are.equal("Test description", scope.contexts.test.description) - end) - - it("should store context with nil fields", function() - scope:set_context("test", { - name = "TestContext", - version = nil, - description = "Test description" - }) - - assert.are.equal("TestContext", scope.contexts.test.name) - assert.is_nil(scope.contexts.test.version) - assert.are.equal("Test description", scope.contexts.test.description) - end) - - it("should overwrite existing context", function() - scope:set_context("test", {name = "Original"}) - scope:set_context("test", {name = "Updated", version = "2.0"}) - - assert.are.equal("Updated", scope.contexts.test.name) - assert.are.equal("2.0", scope.contexts.test.version) - end) - end) - - describe("Event context application", function() - local scope - - before_each(function() - scope = Scope:new() - end) - - it("should apply contexts to event", function() - scope:set_context("os", {name = "TestOS", version = "1.0"}) - scope:set_context("runtime", {name = "lua", version = "5.4"}) - - local event = {} - local updated_event = scope:apply_to_event(event) - - assert.is_not_nil(updated_event.contexts) - assert.are.equal("TestOS", updated_event.contexts.os.name) - assert.are.equal("lua", updated_event.contexts.runtime.name) - end) - - it("should preserve existing event contexts", function() - scope:set_context("os", {name = "TestOS"}) - - local event = { - contexts = { - existing = {name = "Existing"} - } - } - - local updated_event = scope:apply_to_event(event) - - -- Should have both existing and new contexts - assert.are.equal("Existing", updated_event.contexts.existing.name) - assert.are.equal("TestOS", updated_event.contexts.os.name) - end) - - it("should handle empty scope contexts", function() - local event = {message = "test"} - local updated_event = scope:apply_to_event(event) - - -- Event should be unchanged when scope has no contexts - assert.are.equal("test", updated_event.message) - -- Should not add empty contexts object - -- (depends on implementation - might be nil or empty) - end) - end) -end) \ No newline at end of file diff --git a/spec/dsn_parsing_spec.lua b/spec/dsn_parsing_spec.lua index e5a3e0c..8f577ef 100644 --- a/spec/dsn_parsing_spec.lua +++ b/spec/dsn_parsing_spec.lua @@ -1,258 +1,241 @@ describe("DSN Parsing", function() - local dsn_utils - - before_each(function() - dsn_utils = require("sentry.utils.dsn") - end) - - describe("Valid DSN parsing", function() - it("should parse a standard valid DSN with secret", function() - local dsn_string = "https://public_key:secret_key@sentry.io/123456" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("https", dsn.protocol) - assert.are.equal("public_key", dsn.public_key) - assert.are.equal("secret_key", dsn.secret_key) - assert.are.equal("sentry.io", dsn.host) - assert.are.equal(443, dsn.port) - assert.are.equal("/123456", dsn.path) - assert.are.equal("123456", dsn.project_id) - end) - - it("should parse a valid DSN without secret key", function() - local dsn_string = "https://public_key@sentry.io/789" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("https", dsn.protocol) - assert.are.equal("public_key", dsn.public_key) - assert.are.equal("", dsn.secret_key) - assert.are.equal("sentry.io", dsn.host) - assert.are.equal("789", dsn.project_id) - end) - - it("should parse HTTP DSN with default port 80", function() - local dsn_string = "http://public_key@localhost/456" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("http", dsn.protocol) - assert.are.equal("localhost", dsn.host) - assert.are.equal(80, dsn.port) - assert.are.equal("456", dsn.project_id) - end) - - it("should parse DSN with custom port", function() - local dsn_string = "https://public_key@sentry.example.com:9000/123" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("sentry.example.com", dsn.host) - assert.are.equal(9000, dsn.port) - end) - - it("should parse DSN with path prefix", function() - local dsn_string = "https://public_key@sentry.io/path/to/123" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("/path/to/123", dsn.path) - assert.are.equal("123", dsn.project_id) - end) - - it("should parse DSN with subdomain", function() - local dsn_string = "https://abc123@org.ingest.sentry.io/456789" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("abc123", dsn.public_key) - assert.are.equal("org.ingest.sentry.io", dsn.host) - assert.are.equal("456789", dsn.project_id) - end) - - it("should handle numeric project IDs correctly", function() - local dsn_string = "https://key@host.com/1234567890" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("1234567890", dsn.project_id) - end) - end) - - describe("Invalid DSN parsing", function() - it("should reject nil DSN", function() - local dsn, error = dsn_utils.parse_dsn(nil) - - assert.is_not_nil(error) - assert.are.equal("DSN is required", error) - end) - - it("should reject empty string DSN", function() - local dsn, error = dsn_utils.parse_dsn("") - - assert.is_not_nil(error) - assert.are.equal("DSN is required", error) - end) - - it("should reject DSN without protocol", function() - local dsn, error = dsn_utils.parse_dsn("public_key@sentry.io/123") - - assert.is_not_nil(error) - assert.are.equal("Invalid DSN format", error) - end) - - it("should reject DSN without public key", function() - local dsn, error = dsn_utils.parse_dsn("https://@sentry.io/123") - - assert.is_not_nil(error) - assert.are.equal("Invalid DSN format", error) - end) - - it("should reject DSN without host", function() - local dsn, error = dsn_utils.parse_dsn("https://public_key@/123") - - assert.is_not_nil(error) - assert.are.equal("Invalid DSN format", error) - end) - - it("should reject DSN without path", function() - local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io") - - assert.is_not_nil(error) - assert.are.equal("Invalid DSN format", error) - end) - - it("should reject DSN without project ID", function() - local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io/") - - assert.is_not_nil(error) - assert.are.equal("Could not extract project ID from DSN", error) - end) - - it("should reject DSN with non-numeric project ID", function() - local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io/project") - - assert.is_not_nil(error) - assert.are.equal("Could not extract project ID from DSN", error) - end) - - it("should reject malformed URLs", function() - local malformed_dsns = { - "not-a-url", - "://missing-scheme", - "https://", - "https:///path/only", - "ftp://wrong-scheme@host.com/123", - } - - for _, dsn_string in ipairs(malformed_dsns) do - local dsn, error = dsn_utils.parse_dsn(dsn_string) - assert.is_not_nil(error, "Expected error for DSN: " .. dsn_string) - end - end) - - it("should handle invalid port gracefully", function() - -- This tests the port parsing - invalid ports should fallback to default - local dsn_string = "https://key@host.com:invalid/123" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal(443, dsn.port) -- Should fallback to default HTTPS port - end) - end) - - describe("URL building", function() - it("should build correct ingest URL", function() - local dsn = { - protocol = "https", - host = "sentry.io", - project_id = "123456" - } - - local url = dsn_utils.build_ingest_url(dsn) - assert.are.equal("https://sentry.io/api/123456/store/", url) - end) - - it("should build correct ingest URL for HTTP", function() - local dsn = { - protocol = "http", - host = "localhost", - project_id = "789" - } - - local url = dsn_utils.build_ingest_url(dsn) - assert.are.equal("http://localhost/api/789/store/", url) - end) - end) - - describe("Auth header building", function() - it("should build auth header with secret key", function() - local dsn = { - public_key = "public123", - secret_key = "secret456" - } - - local header = dsn_utils.build_auth_header(dsn) - - -- Check that all required parts are present (version-agnostic) - assert.is_true(header:find("Sentry sentry_version=7") ~= nil) - assert.is_true(header:find("sentry_key=public123") ~= nil) - assert.is_true(header:find("sentry_secret=secret456") ~= nil) - assert.is_true(header:find("sentry_client=sentry%-lua/") ~= nil) -- Version-agnostic check - end) - - it("should build auth header without secret key", function() - local dsn = { - public_key = "public123", - secret_key = "" - } - - local header = dsn_utils.build_auth_header(dsn) - - -- Should not include secret - assert.is_true(header:find("sentry_key=public123") ~= nil) - assert.is_true(header:find("sentry_secret") == nil) - end) - - it("should build auth header with nil secret key", function() - local dsn = { - public_key = "public123", - secret_key = nil - } - - local header = dsn_utils.build_auth_header(dsn) - - -- Should not include secret - assert.is_true(header:find("sentry_key=public123") ~= nil) - assert.is_true(header:find("sentry_secret") == nil) - end) - end) - - describe("Edge cases and security", function() - it("should handle special characters in keys", function() - local dsn_string = "https://key-with.dots_and-dashes@host.com/123" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("key-with.dots_and-dashes", dsn.public_key) - end) - - it("should handle IPv4 addresses", function() - local dsn_string = "https://key@192.168.1.1/123" - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal("192.168.1.1", dsn.host) - end) - - it("should handle very long project IDs", function() - local long_id = "123456789012345678901234567890" - local dsn_string = "https://key@host.com/" .. long_id - local dsn, error = dsn_utils.parse_dsn(dsn_string) - - assert.is_nil(error or (error ~= "" and error or nil)) - assert.are.equal(long_id, dsn.project_id) - end) - end) -end) \ No newline at end of file + local dsn_utils + + before_each(function() dsn_utils = require("sentry.core.dsn") end) + + describe("Valid DSN parsing", function() + it("should parse a standard valid DSN with secret", function() + local dsn_string = "https://public_key:secret_key@sentry.io/123456" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("https", dsn.protocol) + assert.are.equal("public_key", dsn.public_key) + assert.are.equal("secret_key", dsn.secret_key) + assert.are.equal("sentry.io", dsn.host) + assert.are.equal(443, dsn.port) + assert.are.equal("/123456", dsn.path) + assert.are.equal("123456", dsn.project_id) + end) + + it("should parse a valid DSN without secret key", function() + local dsn_string = "https://public_key@sentry.io/789" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("https", dsn.protocol) + assert.are.equal("public_key", dsn.public_key) + assert.are.equal("", dsn.secret_key) + assert.are.equal("sentry.io", dsn.host) + assert.are.equal("789", dsn.project_id) + end) + + it("should parse HTTP DSN with default port 80", function() + local dsn_string = "http://public_key@localhost/456" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("http", dsn.protocol) + assert.are.equal("localhost", dsn.host) + assert.are.equal(80, dsn.port) + assert.are.equal("456", dsn.project_id) + end) + + it("should parse DSN with custom port", function() + local dsn_string = "https://public_key@sentry.example.com:9000/123" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("sentry.example.com", dsn.host) + assert.are.equal(9000, dsn.port) + end) + + it("should parse DSN with path prefix", function() + local dsn_string = "https://public_key@sentry.io/path/to/123" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("/path/to/123", dsn.path) + assert.are.equal("123", dsn.project_id) + end) + + it("should parse DSN with subdomain", function() + local dsn_string = "https://abc123@org.ingest.sentry.io/456789" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("abc123", dsn.public_key) + assert.are.equal("org.ingest.sentry.io", dsn.host) + assert.are.equal("456789", dsn.project_id) + end) + + it("should handle numeric project IDs correctly", function() + local dsn_string = "https://key@host.com/1234567890" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("1234567890", dsn.project_id) + end) + end) + + describe("Invalid DSN parsing", function() + it("should reject nil DSN", function() + local dsn, error = dsn_utils.parse_dsn(nil) + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("DSN is required", error) + end) + + it("should reject empty string DSN", function() + local dsn, error = dsn_utils.parse_dsn("") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("DSN is required", error) + end) + + it("should reject DSN without protocol", function() + local dsn, error = dsn_utils.parse_dsn("public_key@sentry.io/123") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Invalid DSN format", error) + end) + + it("should reject DSN without public key", function() + local dsn, error = dsn_utils.parse_dsn("https://@sentry.io/123") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Invalid DSN format", error) + end) + + it("should reject DSN without host", function() + local dsn, error = dsn_utils.parse_dsn("https://public_key@/123") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Invalid DSN format", error) + end) + + it("should reject DSN without path", function() + local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Invalid DSN format", error) + end) + + it("should reject DSN without project ID", function() + local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io/") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Could not extract project ID from DSN", error) + end) + + it("should reject DSN with non-numeric project ID", function() + local dsn, error = dsn_utils.parse_dsn("https://public_key@sentry.io/project") + + assert.is_nil(dsn) + assert.is_not_nil(error) + assert.are.equal("Could not extract project ID from DSN", error) + end) + + it("should reject malformed URLs", function() + local malformed_dsns = { + "not-a-url", + "://missing-scheme", + "https://", + "https:///path/only", + "ftp://wrong-scheme@host.com/123", + } + + for _, dsn_string in ipairs(malformed_dsns) do + local dsn, error = dsn_utils.parse_dsn(dsn_string) + assert.is_nil(dsn) + assert.is_not_nil(error, "Expected error for DSN: " .. dsn_string) + end + end) + + it("should handle invalid port gracefully", function() + -- This tests the port parsing - invalid ports should fallback to default + local dsn_string = "https://key@host.com:invalid/123" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal(443, dsn.port) -- Should fallback to default HTTPS port + end) + end) + + describe("Auth header building", function() + it("should build auth header with secret key", function() + local dsn = { + public_key = "public123", + secret_key = "secret456", + } + + local header = dsn_utils.build_auth_header(dsn) + + -- Check that all required parts are present (version-agnostic) + assert.is_true(header:find("Sentry sentry_version=7") ~= nil) + assert.is_true(header:find("sentry_key=public123") ~= nil) + assert.is_true(header:find("sentry_secret=secret456") ~= nil) + assert.is_true(header:find("sentry_client=sentry%-lua/") ~= nil) -- Version-agnostic check + end) + + it("should build auth header without secret key", function() + local dsn = { + public_key = "public123", + secret_key = "", + } + + local header = dsn_utils.build_auth_header(dsn) + + -- Should not include secret + assert.is_true(header:find("sentry_key=public123") ~= nil) + assert.is_true(header:find("sentry_secret") == nil) + end) + + it("should build auth header with nil secret key", function() + local dsn = { + public_key = "public123", + secret_key = nil, + } + + local header = dsn_utils.build_auth_header(dsn) + + -- Should not include secret + assert.is_true(header:find("sentry_key=public123") ~= nil) + assert.is_true(header:find("sentry_secret") == nil) + end) + end) + + describe("Edge cases and security", function() + it("should handle special characters in keys", function() + local dsn_string = "https://key-with.dots_and-dashes@host.com/123" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("key-with.dots_and-dashes", dsn.public_key) + end) + + it("should handle IPv4 addresses", function() + local dsn_string = "https://key@192.168.1.1/123" + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal("192.168.1.1", dsn.host) + end) + + it("should handle very long project IDs", function() + local long_id = "123456789012345678901234567890" + local dsn_string = "https://key@host.com/" .. long_id + local dsn, error = dsn_utils.parse_dsn(dsn_string) + + assert.is_nil(error or (error ~= "" and error or nil)) + assert.are.equal(long_id, dsn.project_id) + end) + end) +end) diff --git a/spec/logger_spec.lua b/spec/logger_spec.lua deleted file mode 100644 index 28c813e..0000000 --- a/spec/logger_spec.lua +++ /dev/null @@ -1,404 +0,0 @@ -local logger = require("sentry.logger") -local envelope = require("sentry.utils.envelope") -local json = require("sentry.utils.json") - -describe("sentry.logger", function() - - before_each(function() - -- Reset logger state - if logger.unhook_print then - logger.unhook_print() - end - - -- Clear any existing configuration - logger.init({ - enable_logs = false, - hook_print = false - }) - end) - - describe("initialization", function() - it("should initialize with default config", function() - logger.init({ - enable_logs = true - }) - - local config = logger.get_config() - assert.is_true(config.enable_logs) - assert.equals(100, config.max_buffer_size) - assert.equals(5.0, config.flush_timeout) - assert.is_false(config.hook_print) - end) - - it("should initialize with custom config", function() - logger.init({ - enable_logs = true, - max_buffer_size = 50, - flush_timeout = 10.0, - hook_print = true - }) - - local config = logger.get_config() - assert.is_true(config.enable_logs) - assert.equals(50, config.max_buffer_size) - assert.equals(10.0, config.flush_timeout) - assert.is_true(config.hook_print) - end) - - it("should support before_send_log hook", function() - local hook_called = false - local modified_body = "modified message" - - logger.init({ - enable_logs = true, - before_send_log = function(log_record) - hook_called = true - log_record.body = modified_body - return log_record - end - }) - - logger.info("original message") - logger.flush() - - assert.is_true(hook_called) - end) - - it("should filter logs when before_send_log returns nil", function() - logger.init({ - enable_logs = true, - before_send_log = function(log_record) - if log_record.level == "debug" then - return nil -- Filter debug logs - end - return log_record - end - }) - - logger.debug("This should be filtered") - logger.info("This should pass through") - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) -- Only info log should remain - end) - end) - - describe("logging levels", function() - before_each(function() - logger.init({ - enable_logs = true, - max_buffer_size = 10 - }) - end) - - it("should support all log levels", function() - logger.trace("trace message") - logger.debug("debug message") - logger.info("info message") - logger.warn("warn message") - logger.error("error message") - logger.fatal("fatal message") - - local status = logger.get_buffer_status() - assert.equals(6, status.logs) - end) - - it("should not log when disabled", function() - logger.init({ - enable_logs = false - }) - - logger.info("This should not be logged") - - local status = logger.get_buffer_status() - assert.equals(0, status.logs) - end) - end) - - describe("structured logging", function() - before_each(function() - logger.init({ - enable_logs = true, - max_buffer_size = 10 - }) - end) - - it("should handle parameterized messages", function() - logger.info("User %s performed action %s", {"user123", "login"}) - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - - it("should handle messages with attributes", function() - logger.info("Order processed", nil, { - order_id = "order123", - amount = 99.99, - success = true - }) - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - - it("should handle both parameters and attributes", function() - logger.warn("Payment failed for user %s", {"user456"}, { - error_code = "DECLINED", - attempts = 3 - }) - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - end) - - describe("buffer management", function() - it("should flush when buffer exceeds max size", function() - local flush_called = false - - logger.init({ - enable_logs = true, - max_buffer_size = 2 - }) - - -- Mock flush to detect when it's called - local original_flush = logger.flush - logger.flush = function() - flush_called = true - original_flush() - end - - logger.info("message 1") - assert.is_false(flush_called) - - logger.info("message 2") - assert.is_true(flush_called) - end) - - it("should track buffer status correctly", function() - logger.init({ - enable_logs = true, - max_buffer_size = 5 - }) - - local initial_status = logger.get_buffer_status() - assert.equals(0, initial_status.logs) - - logger.info("test message 1") - logger.info("test message 2") - - local status = logger.get_buffer_status() - assert.equals(2, status.logs) - assert.equals(5, status.max_size) - end) - - it("should clear buffer after flush", function() - logger.init({ - enable_logs = true - }) - - logger.info("test message") - local pre_flush_status = logger.get_buffer_status() - assert.equals(1, pre_flush_status.logs) - - logger.flush() - - local post_flush_status = logger.get_buffer_status() - assert.equals(0, post_flush_status.logs) - end) - end) - - describe("print hooking", function() - it("should hook print when enabled", function() - logger.init({ - enable_logs = true, - hook_print = true - }) - - local original_print_type = type(_G.print) - assert.equals("function", original_print_type) - - -- Print hooking changes print function - logger.hook_print() - - print("test message") - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - - it("should unhook print correctly", function() - logger.init({ - enable_logs = true, - hook_print = true - }) - - logger.hook_print() - local hooked_print = _G.print - - logger.unhook_print() - - -- Should restore original print - assert.is_not.equals(hooked_print, _G.print) - end) - - it("should handle multiple print arguments", function() - logger.init({ - enable_logs = true, - hook_print = true - }) - - logger.hook_print() - - print("arg1", "arg2", 123, true) - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - - it("should not create infinite loops", function() - logger.init({ - enable_logs = true, - hook_print = true - }) - - logger.hook_print() - - -- This should not cause infinite recursion - logger.info("This log might trigger internal print statements") - - -- If we get here without stack overflow, the recursion protection works - assert.is_true(true) - end) - end) - - describe("trace correlation", function() - it("should include trace_id when tracing is available", function() - -- This test would need mock tracing module - -- For now just verify it doesn't crash without tracing - logger.init({ - enable_logs = true - }) - - logger.info("test message") - - local status = logger.get_buffer_status() - assert.equals(1, status.logs) - end) - end) -end) - -describe("sentry.utils.envelope log support", function() - - describe("build_log_envelope", function() - it("should create valid log envelope", function() - local log_records = { - { - timestamp = 1234567890.5, - trace_id = "abc123", - level = "info", - body = "Test message", - attributes = { - ["test.key"] = {value = "test_value", type = "string"} - }, - severity_number = 9 - } - } - - local envelope_body = envelope.build_log_envelope(log_records) - - assert.is_string(envelope_body) - assert.is_not.equals("", envelope_body) - - -- Should contain log envelope structure - assert.is_truthy(envelope_body:find('"type":"log"')) - assert.is_truthy(envelope_body:find('"item_count":1')) - assert.is_truthy(envelope_body:find('items%.log')) - end) - - it("should handle multiple log records", function() - local log_records = { - { - timestamp = 1234567890.1, - trace_id = "trace1", - level = "info", - body = "Message 1", - attributes = {}, - severity_number = 9 - }, - { - timestamp = 1234567890.2, - trace_id = "trace2", - level = "error", - body = "Message 2", - attributes = {}, - severity_number = 17 - } - } - - local envelope_body = envelope.build_log_envelope(log_records) - - assert.is_string(envelope_body) - assert.is_truthy(envelope_body:find('"item_count":2')) - end) - - it("should return empty string for empty log array", function() - local envelope_body = envelope.build_log_envelope({}) - assert.equals("", envelope_body) - end) - - it("should return empty string for nil input", function() - local envelope_body = envelope.build_log_envelope(nil) - assert.equals("", envelope_body) - end) - - it("should include proper envelope structure", function() - local log_records = { - { - timestamp = 1234567890.5, - trace_id = "abc123", - level = "warn", - body = "Warning message", - attributes = { - ["severity"] = {value = "medium", type = "string"}, - ["count"] = {value = 42, type = "integer"} - }, - severity_number = 13 - } - } - - local envelope_body = envelope.build_log_envelope(log_records) - - -- Split envelope into lines - local lines = {} - for line in envelope_body:gmatch("[^\n]+") do - table.insert(lines, line) - end - - -- Should have 3 lines: header, item header, payload - assert.equals(3, #lines) - - -- Parse header - local header = json.decode(lines[1]) - assert.is_string(header.sent_at) - - -- Parse item header - local item_header = json.decode(lines[2]) - assert.equals("log", item_header.type) - assert.equals(1, item_header.item_count) - assert.equals("application/vnd.sentry.items.log+json", item_header.content_type) - - -- Parse payload - local payload = json.decode(lines[3]) - assert.is_table(payload.items) - assert.equals(1, #payload.items) - - local log_item = payload.items[1] - assert.equals("abc123", log_item.trace_id) - assert.equals("warn", log_item.level) - assert.equals("Warning message", log_item.body) - assert.equals(13, log_item.severity_number) - assert.is_table(log_item.attributes) - end) - end) -end) \ No newline at end of file diff --git a/spec/os_detection_spec.lua b/spec/os_detection_spec.lua deleted file mode 100644 index d5dbb9a..0000000 --- a/spec/os_detection_spec.lua +++ /dev/null @@ -1,168 +0,0 @@ -describe("OS Detection", function() - local os_utils - local original_detectors - - before_each(function() - -- Load the OS utils module - os_utils = require("sentry.utils.os") - - -- Save original detectors and clear them for testing - original_detectors = {} - for i, detector in ipairs(os_utils.detectors or {}) do - table.insert(original_detectors, detector) - end - - -- Clear detectors for clean test state - if os_utils.detectors then - for i = #os_utils.detectors, 1, -1 do - table.remove(os_utils.detectors, i) - end - end - end) - - after_each(function() - -- Restore original detectors - if os_utils.detectors then - for i = #os_utils.detectors, 1, -1 do - table.remove(os_utils.detectors, i) - end - for _, detector in ipairs(original_detectors) do - table.insert(os_utils.detectors, detector) - end - end - end) - - describe("detector registration", function() - it("should register a new detector", function() - local test_detector = { - detect = function() - return {name = "TestOS", version = "1.0"} - end - } - - os_utils.register_detector(test_detector) - - -- Verify detector was registered - local os_info = os_utils.get_os_info() - assert.are.equal("TestOS", os_info.name) - assert.are.equal("1.0", os_info.version) - end) - - it("should try detectors in registration order", function() - local first_detector = { - detect = function() - return {name = "FirstOS", version = "1.0"} - end - } - local second_detector = { - detect = function() - return {name = "SecondOS", version = "2.0"} - end - } - - os_utils.register_detector(first_detector) - os_utils.register_detector(second_detector) - - -- Should return result from first successful detector - local os_info = os_utils.get_os_info() - assert.are.equal("FirstOS", os_info.name) - end) - end) - - describe("nil version handling", function() - it("should handle detector returning nil version", function() - local detector_with_nil = { - detect = function() - return {name = "TestOS", version = nil} - end - } - - os_utils.register_detector(detector_with_nil) - - local os_info = os_utils.get_os_info() - assert.are.equal("TestOS", os_info.name) - assert.is_nil(os_info.version) - end) - - it("should handle detector returning no version field", function() - local detector_no_version = { - detect = function() - return {name = "TestOS"} - end - } - - os_utils.register_detector(detector_no_version) - - local os_info = os_utils.get_os_info() - assert.are.equal("TestOS", os_info.name) - assert.is_nil(os_info.version) - end) - end) - - describe("failed detection", function() - it("should return nil when no detectors are registered", function() - -- No detectors registered - local os_info = os_utils.get_os_info() - assert.is_nil(os_info) - end) - - it("should return nil when all detectors fail", function() - local failing_detector = { - detect = function() - return nil - end - } - - os_utils.register_detector(failing_detector) - - local os_info = os_utils.get_os_info() - assert.is_nil(os_info) - end) - - it("should handle detector that throws error", function() - local error_detector = { - detect = function() - error("Detection failed!") - end - } - local working_detector = { - detect = function() - return {name = "WorkingOS", version = "1.0"} - end - } - - os_utils.register_detector(error_detector) - os_utils.register_detector(working_detector) - - -- Should skip the error detector and use the working one - local os_info = os_utils.get_os_info() - assert.are.equal("WorkingOS", os_info.name) - end) - end) - - describe("detector interface", function() - it("should require detect function", function() - local invalid_detector = { - -- Missing detect function - } - - -- Register detector without error - os_utils.register_detector(invalid_detector) - - -- But it should fail when trying to use it - local os_info = os_utils.get_os_info() - assert.is_nil(os_info) - end) - - it("should handle detector with invalid detect function", function() - local invalid_detector = { - detect = "not a function" - } - - os_utils.register_detector(invalid_detector) - - local os_info = os_utils.get_os_info() - assert.is_nil(os_info) - end) - end) -end) \ No newline at end of file diff --git a/spec/performance_tracing_integration_spec.lua b/spec/performance_tracing_integration_spec.lua deleted file mode 100644 index 929d0dc..0000000 --- a/spec/performance_tracing_integration_spec.lua +++ /dev/null @@ -1,326 +0,0 @@ --- Integration tests for performance module and tracing propagation --- These tests would have caught the distributed tracing bug - -local performance = require("sentry.performance") -local propagation = require("sentry.tracing.propagation") -local tracing = require("sentry.tracing") - -describe("performance module tracing integration", function() - before_each(function() - -- Clear any existing context and transactions - propagation.clear_context() - end) - - after_each(function() - -- Clean up after each test - propagation.clear_context() - end) - - describe("new transaction creation", function() - it("should create new trace when no propagation context exists", function() - -- No existing trace context - assert.is_nil(propagation.get_current_context()) - - local transaction = performance.start_transaction("test_transaction", "task") - - assert.is_not_nil(transaction) - assert.is_not_nil(transaction.trace_id) - assert.is_not_nil(transaction.span_id) - assert.is_nil(transaction.parent_span_id) -- Root transaction - - -- Should update propagation context - local context = propagation.get_current_context() - assert.is_not_nil(context) - assert.are.equal(transaction.trace_id, context.trace_id) - assert.are.equal(transaction.span_id, context.span_id) - - transaction:finish("ok") - end) - - it("should continue existing trace when propagation context exists", function() - -- Set up existing trace context (simulating incoming request) - local existing_context = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - parent_span_id = nil, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - propagation.set_current_context(existing_context) - - -- Start transaction - should continue existing trace - local transaction = performance.start_transaction("continued_transaction", "http.server") - - assert.is_not_nil(transaction) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", transaction.trace_id) -- Same trace ID - assert.are.equal("1000000000000000", transaction.parent_span_id) -- Previous span becomes parent - assert.are.not_equal("1000000000000000", transaction.span_id) -- New span ID for transaction - - -- Should update propagation context with new span - local updated_context = propagation.get_current_context() - assert.are.equal(transaction.trace_id, updated_context.trace_id) - assert.are.equal(transaction.span_id, updated_context.span_id) - assert.are.equal(transaction.parent_span_id, updated_context.parent_span_id) - - transaction:finish("ok") - end) - - it("should generate headers for outgoing requests during transaction", function() - local transaction = performance.start_transaction("client_transaction", "http.client") - - -- Should be able to get trace headers for outgoing requests - local headers = tracing.get_request_headers("http://api.example.com") - - assert.is_not_nil(headers) - assert.is_not_nil(headers["sentry-trace"]) - assert.is_not_nil(headers["sentry-trace"]:match(transaction.trace_id)) - assert.is_not_nil(headers["sentry-trace"]:match(transaction.span_id)) - - transaction:finish("ok") - end) - - it("should maintain trace context across spans", function() - local transaction = performance.start_transaction("parent_transaction", "task") - local original_trace_id = transaction.trace_id - - -- Start nested span - local span = transaction:start_span("db.query", "SELECT * FROM users") - - -- Trace ID should remain the same, span ID should change - local context_during_span = propagation.get_current_context() - assert.are.equal(original_trace_id, context_during_span.trace_id) - assert.are.equal(span.span_id, context_during_span.span_id) -- Current span is active - assert.are.equal(transaction.span_id, span.parent_span_id) -- Transaction is parent - - span:finish("ok") - - -- Context should revert to transaction after span finishes - local context_after_span = propagation.get_current_context() - assert.are.equal(original_trace_id, context_after_span.trace_id) - assert.are.equal(transaction.span_id, context_after_span.span_id) -- Back to transaction - - transaction:finish("ok") - end) - end) - - describe("distributed tracing workflow", function() - it("should simulate complete client-server distributed trace", function() - -- === CLIENT SIDE === - -- Start client transaction - local client_tx = performance.start_transaction("http_request", "http.client") - local original_trace_id = client_tx.trace_id - - -- Get headers for outgoing request - local outgoing_headers = tracing.get_request_headers("http://server.com/api") - assert.is_not_nil(outgoing_headers["sentry-trace"]) - - -- Parse the sentry-trace header to simulate what server receives - local sentry_headers = require("sentry.tracing.headers") - local trace_data = sentry_headers.parse_sentry_trace(outgoing_headers["sentry-trace"]) - assert.is_not_nil(trace_data) - assert.are.equal(original_trace_id, trace_data.trace_id) - assert.are.equal(client_tx.span_id, trace_data.span_id) - - -- === SERVER SIDE === - -- Simulate server receiving request and continuing trace - local incoming_headers = { - ["sentry-trace"] = outgoing_headers["sentry-trace"] - } - - -- Server continues trace from headers - local server_context = propagation.continue_trace_from_headers(incoming_headers) - assert.are.equal(original_trace_id, server_context.trace_id) -- Same trace - assert.are.equal(client_tx.span_id, server_context.parent_span_id) -- Client span becomes parent - - -- Server starts transaction - should continue the distributed trace - local server_tx = performance.start_transaction("handle_request", "http.server") - assert.are.equal(original_trace_id, server_tx.trace_id) -- Same distributed trace - -- The server transaction's parent should be the server's propagation context span_id - -- which was created from the client's span, so they're related but not identical - assert.is_not_nil(server_tx.parent_span_id) -- Connected through propagation context - - -- Server finishes its work - server_tx:finish("ok") - - -- === CLIENT SIDE CLEANUP === - -- Restore client context and finish client transaction - local client_context = { - trace_id = client_tx.trace_id, - span_id = client_tx.span_id, - parent_span_id = client_tx.parent_span_id, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - propagation.set_current_context(client_context) - client_tx:finish("ok") - - -- Note: In our current implementation, the server starts a new transaction - -- that continues the trace but creates a new transaction context. - -- The key is that the trace_id should be the same and parent_span_id should be correct. - assert.are.equal(client_tx.trace_id, server_tx.trace_id) - end) - - it("should handle multiple incoming trace formats", function() - -- Test W3C traceparent format - local w3c_headers = { - ["traceparent"] = "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-01" - } - - propagation.continue_trace_from_headers(w3c_headers) - local tx1 = performance.start_transaction("w3c_continuation", "http.server") - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", tx1.trace_id) - tx1:finish("ok") - - -- Test sentry-trace format - local sentry_headers = { - ["sentry-trace"] = "75302ac48a024bde9a3b3734a82e36c8-2000000000000000-1" - } - - propagation.continue_trace_from_headers(sentry_headers) - local tx2 = performance.start_transaction("sentry_continuation", "http.server") - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", tx2.trace_id) - -- Note: The parent_span_id in our implementation will be the span_id from the - -- propagation context, not the original incoming span_id - assert.is_not_nil(tx2.parent_span_id) - tx2:finish("ok") - end) - end) - - describe("trace propagation targeting integration", function() - it("should respect trace propagation targets when getting headers", function() - -- Initialize tracing with specific targets - tracing.init({ - trace_propagation_targets = {"api.allowed.com"} - }) - - local transaction = performance.start_transaction("client_request", "http.client") - - -- Should propagate to allowed target - local allowed_headers = tracing.get_request_headers("https://api.allowed.com/endpoint") - assert.is_not_nil(allowed_headers["sentry-trace"]) - - -- Should not propagate to disallowed target - local blocked_headers = tracing.get_request_headers("https://api.blocked.com/endpoint") - -- The table should be empty but still a table - assert.are.equal("table", type(blocked_headers)) - assert.is_nil(blocked_headers["sentry-trace"]) - - transaction:finish("ok") - end) - - it("should propagate to all targets with wildcard", function() - tracing.init({ - trace_propagation_targets = {"*"} - }) - - local transaction = performance.start_transaction("client_request", "http.client") - - -- Should propagate to any target with wildcard - local headers1 = tracing.get_request_headers("https://api1.com/endpoint") - local headers2 = tracing.get_request_headers("https://api2.com/endpoint") - - assert.is_not_nil(headers1["sentry-trace"]) - assert.is_not_nil(headers2["sentry-trace"]) - assert.is_not_nil(headers1["sentry-trace"]:match(transaction.trace_id)) - assert.is_not_nil(headers2["sentry-trace"]:match(transaction.trace_id)) - - transaction:finish("ok") - end) - end) - - describe("error cases and edge conditions", function() - it("should handle malformed incoming trace headers gracefully", function() - local bad_headers = { - ["sentry-trace"] = "malformed-header-data" - } - - -- Should not crash, should create new trace - propagation.continue_trace_from_headers(bad_headers) - local transaction = performance.start_transaction("recovery_transaction", "http.server") - - assert.is_not_nil(transaction) - assert.is_not_nil(transaction.trace_id) - -- With malformed headers, it creates a new trace continuation context - assert.is_not_nil(transaction.parent_span_id) - - transaction:finish("ok") - end) - - it("should handle missing trace context during header generation", function() - -- Clear any context - propagation.clear_context() - - -- Should return empty headers when no context - local headers = tracing.get_request_headers("http://example.com") - assert.is_nil(headers["sentry-trace"]) - end) - - it("should maintain trace consistency with nested operations", function() - local transaction = performance.start_transaction("complex_operation", "task") - local trace_id = transaction.trace_id - - -- Multiple nested spans - local span1 = transaction:start_span("step1", "Process data") - assert.are.equal(trace_id, span1.trace_id) - - local span2 = transaction:start_span("step2", "Validate data") - assert.are.equal(trace_id, span2.trace_id) - assert.are.equal(span1.span_id, span2.parent_span_id) - - -- Headers should always contain current trace - local headers_during_nested = tracing.get_request_headers("http://api.com") - assert.is_not_nil(headers_during_nested["sentry-trace"]:match(trace_id)) - assert.is_not_nil(headers_during_nested["sentry-trace"]:match(span2.span_id)) - - span2:finish("ok") - span1:finish("ok") - transaction:finish("ok") - end) - end) - - describe("performance regression tests", function() - it("should handle rapid transaction creation/destruction", function() - -- This test would catch memory leaks or context corruption - for i = 1, 10 do - local tx = performance.start_transaction("rapid_tx_" .. i, "task") - local context = propagation.get_current_context() - - assert.is_not_nil(context) - assert.are.equal(tx.trace_id, context.trace_id) - - tx:finish("ok") - end - - -- Context should be clean after all transactions (or have the last context) - local final_context = propagation.get_current_context() - -- The context might still exist but should be consistent - assert.is_not_nil(final_context) - end) - - it("should handle concurrent-style trace context switching", function() - -- Simulate context switching (like coroutines/async) - local tx1 = performance.start_transaction("tx1", "task") - local context1 = propagation.get_current_context() - - -- Switch to different context - local tx2 = performance.start_transaction("tx2", "task") - local context2 = propagation.get_current_context() - - -- In our current implementation, the second transaction might continue from the first - -- This is actually good behavior for distributed tracing - -- assert.are.not_equal(context1.trace_id, context2.trace_id) - - -- Switch back to first context (simulating async resume) - propagation.set_current_context(context1) - local resumed_context = propagation.get_current_context() - assert.are.equal(context1.trace_id, resumed_context.trace_id) - - -- Clean up both - tx1:finish("ok") -- tx1 context - propagation.set_current_context(context2) - tx2:finish("ok") -- tx2 context - end) - end) -end) \ No newline at end of file diff --git a/spec/platform_detection_spec.lua b/spec/platform_detection_spec.lua deleted file mode 100644 index c8d319e..0000000 --- a/spec/platform_detection_spec.lua +++ /dev/null @@ -1,192 +0,0 @@ -describe("Platform Detection", function() - -- Note: These tests focus on the platform detection logic rather than - -- executing actual system commands, to ensure tests are portable and reliable - - describe("Desktop platform detection", function() - local desktop_platform - - before_each(function() - -- We can't easily test the actual standard platform detection since it relies on - -- system commands, but we can test the module structure - desktop_platform = require("sentry.platforms.standard.os_detection") - end) - - it("should have detect_os function", function() - assert.is_function(desktop_platform.detect_os) - end) - - it("should return nil or valid OSInfo", function() - local result = desktop_platform.detect_os() - - if result then - -- If detection succeeded, should have name - assert.is_string(result.name) - -- Version can be nil or string - assert.is_true(result.version == nil or type(result.version) == "string") - else - -- If detection failed, should be nil - assert.is_nil(result) - end - end) - end) - - describe("Roblox platform detection", function() - local roblox_platform - local original_G - - before_each(function() - roblox_platform = require("sentry.platforms.roblox.os_detection") - original_G = _G - end) - - after_each(function() - _G = original_G - end) - - it("should have detect_os function", function() - assert.is_function(roblox_platform.detect_os) - end) - - it("should return nil when not in Roblox environment", function() - -- Ensure we're not in Roblox environment - _G.game = nil - - local result = roblox_platform.detect_os() - assert.is_nil(result) - end) - - it("should detect Roblox when game and GetService exist", function() - -- Mock Roblox environment - _G.game = { - GetService = function() return {} end - } - - local result = roblox_platform.detect_os() - - assert.is_not_nil(result) - assert.are.equal("Roblox", result.name) - assert.is_nil(result.version) -- Should be nil, not empty string - end) - - it("should return nil when game exists but no GetService", function() - _G.game = {} -- No GetService method - - local result = roblox_platform.detect_os() - assert.is_nil(result) - end) - end) - - describe("LÖVE 2D platform detection", function() - local love2d_platform - local original_love - - before_each(function() - love2d_platform = require("sentry.platforms.love2d.os_detection") - original_love = _G.love - end) - - after_each(function() - _G.love = original_love - end) - - it("should have detect_os function", function() - assert.is_function(love2d_platform.detect_os) - end) - - it("should return nil when not in LÖVE environment", function() - _G.love = nil - - local result = love2d_platform.detect_os() - assert.is_nil(result) - end) - - it("should detect OS when LÖVE system is available", function() - _G.love = { - system = { - getOS = function() return "Windows" end - } - } - - local result = love2d_platform.detect_os() - - assert.is_not_nil(result) - assert.are.equal("Windows", result.name) - assert.is_nil(result.version) -- Should be nil, not empty string - end) - - it("should return nil when love.system.getOS returns nil", function() - _G.love = { - system = { - getOS = function() return nil end - } - } - - local result = love2d_platform.detect_os() - assert.is_nil(result) - end) - - it("should handle missing love.system", function() - _G.love = {} -- No system module - - local result = love2d_platform.detect_os() - assert.is_nil(result) - end) - end) - - describe("Nginx platform detection", function() - local nginx_platform - local original_ngx - - before_each(function() - nginx_platform = require("sentry.platforms.nginx.os_detection") - original_ngx = _G.ngx - end) - - after_each(function() - _G.ngx = original_ngx - end) - - it("should have detect_os function", function() - assert.is_function(nginx_platform.detect_os) - end) - - it("should return nil when not in nginx environment", function() - _G.ngx = nil - - local result = nginx_platform.detect_os() - assert.is_nil(result) - end) - - -- Note: Testing nginx detection fully would require mocking io.popen - -- which is complex. We mainly test the environment detection logic. - end) - - describe("Platform loader", function() - it("should load platform modules without error", function() - -- This test ensures platform_loader can be required and executes without error - assert.has_no.errors(function() - require("sentry.platform_loader") - end) - end) - end) - - describe("Integration test", function() - it("should have at least one working detector after loading platforms", function() - -- Load platform loader to register detectors - require("sentry.platform_loader") - - local os_utils = require("sentry.utils.os") - local result = os_utils.get_os_info() - - -- Should detect at least the desktop platform on most systems - -- If this fails, it might be running in a very limited environment - if result then - assert.is_string(result.name) - -- Version can be nil - assert.is_true(result.version == nil or type(result.version) == "string") - end - -- Note: We don't assert result is not nil because the test might run - -- in an environment where no detectors work - end) - end) -end) \ No newline at end of file diff --git a/spec/platforms/love2d/conf.lua b/spec/platforms/love2d/conf.lua deleted file mode 100644 index ef17791..0000000 --- a/spec/platforms/love2d/conf.lua +++ /dev/null @@ -1,45 +0,0 @@ --- Minimal Love2D configuration for headless testing - -function love.conf(t) - t.identity = "sentry-love2d-tests" - t.version = "11.5" - t.console = false - - -- Minimal headless window configuration - -- Note: t.window = false doesn't work reliably across all Love2D versions - -- Instead use minimal window config that works in headless environments - t.window.title = "Sentry Love2D Tests (Headless)" - t.window.width = 1 - t.window.height = 1 - t.window.borderless = true - t.window.resizable = false - t.window.minwidth = 1 - t.window.minheight = 1 - t.window.fullscreen = false - t.window.fullscreentype = "desktop" - t.window.vsync = 0 - t.window.display = 1 - t.window.highdpi = false - t.window.x = nil - t.window.y = nil - - -- Disable all non-essential modules for headless testing - t.modules.audio = false - t.modules.data = false - t.modules.event = true - t.modules.font = false - t.modules.graphics = true -- Keep minimal graphics for headless compatibility - t.modules.image = false - t.modules.joystick = false - t.modules.keyboard = false - t.modules.math = false - t.modules.mouse = false - t.modules.physics = false - t.modules.sound = false - t.modules.system = true - t.modules.thread = true - t.modules.timer = true - t.modules.touch = false - t.modules.video = false - t.modules.window = true -- Keep window module for headless compatibility -end \ No newline at end of file diff --git a/spec/platforms/love2d/https_connectivity_spec.lua b/spec/platforms/love2d/https_connectivity_spec.lua deleted file mode 100644 index 5b435ed..0000000 --- a/spec/platforms/love2d/https_connectivity_spec.lua +++ /dev/null @@ -1,119 +0,0 @@ --- Love2D HTTPS connectivity and lua-https integration tests -describe("Love2D HTTPS connectivity", function() - local sentry - local http - - setup(function() - -- Mock Love2D environment - _G.love = { - filesystem = { - write = function(filename, data) - return true - end - } - } - - package.path = "build/?.lua;build/?/init.lua;" .. package.path - sentry = require("sentry") - http = require("sentry.utils.http") - end) - - teardown(function() - _G.love = nil - end) - - describe("HTTP client functionality", function() - it("should successfully make HTTPS requests to test endpoints", function() - local response = http.request({ - url = "https://httpbin.org/get", - method = "GET", - timeout = 10 - }) - - assert.is_true(response.success) - assert.equals(200, response.status) - end) - - it("should handle HTTPS request failures gracefully", function() - local response = http.request({ - url = "https://nonexistent-domain-for-testing.com/endpoint", - method = "GET", - timeout = 5 - }) - - assert.is_false(response.success) - assert.is_not_nil(response.error) - end) - end) - - describe("Sentry integration", function() - it("should initialize successfully in Love2D environment", function() - local result = sentry.init({ - dsn = "https://testkey@test.ingest.sentry.io/123456", - environment = "love2d-test", - debug = true - }) - - assert.is_not_nil(sentry._client) - end) - - it("should capture messages with proper error handling", function() - sentry.init({ - dsn = "https://testkey@test.ingest.sentry.io/123456", - environment = "love2d-spec-test" - }) - - -- Should not throw errors even with invalid DSN in test - local ok = pcall(function() - sentry.capture_message("Test message from Love2D spec", "info") - end) - - assert.is_true(ok) - end) - - it("should capture exceptions with stack traces", function() - sentry.init({ - dsn = "https://testkey@test.ingest.sentry.io/123456", - environment = "love2d-spec-test" - }) - - local function level3() - error("Test error for stack trace validation") - end - - local function level2() - level3() - end - - local function level1() - level2() - end - - -- Should capture exception without crashing - local ok, err = pcall(level1) - assert.is_false(ok) - - local capture_ok = pcall(function() - sentry.capture_exception(err) - end) - - assert.is_true(capture_ok) - end) - end) - - describe("Love2D transport selection", function() - it("should detect Love2D environment properly", function() - local love2d_transport = require("sentry.platforms.love2d.transport") - - -- Should detect Love2D environment when love global exists - assert.is_true(love2d_transport.is_love2d_available()) - - -- Should be able to create transport - local transport = love2d_transport.create_love2d_transport({ - dsn = "https://testkey@test.ingest.sentry.io/123456" - }) - - assert.is_not_nil(transport) - end) - end) -end) \ No newline at end of file diff --git a/spec/platforms/love2d/love2d_spec.lua b/spec/platforms/love2d/love2d_spec.lua deleted file mode 100644 index 849d83c..0000000 --- a/spec/platforms/love2d/love2d_spec.lua +++ /dev/null @@ -1,291 +0,0 @@ --- Love2D platform unit tests --- These run with busted but mock the Love2D environment - -describe("sentry.platforms.love2d", function() - - -- Mock Love2D environment - local original_love - - before_each(function() - original_love = _G.love - - -- Mock Love2D global - _G.love = { - system = { - getOS = function() return "macOS" end - }, - getVersion = function() return 11, 5, 0, "Mysterious Mysteries" end, - graphics = { - getDimensions = function() return 800, 600 end - }, - thread = { - newThread = function(code) - return { - start = function() end, - wait = function() end - } - end, - getChannel = function(name) - return { - push = function() end, - pop = function() return nil end - } - end - }, - filesystem = { - append = function() return true end - }, - timer = { - sleep = function() end, - getTime = function() return 0 end - } - } - end) - - after_each(function() - _G.love = original_love - end) - - describe("transport", function() - local transport_module - - before_each(function() - package.loaded["sentry.platforms.love2d.transport"] = nil - transport_module = require("sentry.platforms.love2d.transport") - end) - - it("should detect Love2D availability", function() - assert.is_true(transport_module.is_love2d_available()) - end) - - it("should create Love2D transport", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - assert.is_not_nil(transport) - assert.is_function(transport.send) - end) - - it("should queue events for async sending", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - local success, message = transport:send({event_id = "test123"}) - - assert.is_true(success) - assert.matches("queued", message) - end) - - it("should queue envelopes for async sending", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - local success, message = transport:send_envelope("test envelope data") - - assert.is_true(success) - assert.matches("queued", message) - end) - - it("should handle flush operation", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - - -- Should not error - assert.has_no.errors(function() - transport:flush() - end) - end) - - it("should queue events but not process them without lua-https", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - - -- Queue some events - local test_event = {event_id = "test123", message = "test message"} - transport:send(test_event) - - -- Verify event was queued - assert.equals(1, #transport.event_queue) - - -- Flush should return early without lua-https, leaving queues intact - transport:flush() - - -- Queue should still have items since lua-https is not available - assert.equals(1, #transport.event_queue) - end) - - it("should queue envelopes but not process them without lua-https", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - - -- Queue some envelopes - local test_envelope = "test envelope data" - transport:send_envelope(test_envelope) - - -- Verify envelope was queued - assert.equals(1, #transport.envelope_queue) - - -- Flush should return early without lua-https, leaving queues intact - transport:flush() - - -- Queue should still have items since lua-https is not available - assert.equals(1, #transport.envelope_queue) - end) - - it("should manage both event and envelope queues independently", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - - -- Queue multiple items - transport:send({event_id = "event1"}) - transport:send({event_id = "event2"}) - transport:send_envelope("envelope1") - transport:send_envelope("envelope2") - - -- Verify items were queued - assert.equals(2, #transport.event_queue) - assert.equals(2, #transport.envelope_queue) - - -- Without lua-https, flush returns early and queues remain - transport:flush() - - -- Both queues should still have items - assert.equals(2, #transport.event_queue) - assert.equals(2, #transport.envelope_queue) - end) - - it("should handle close operation", function() - local config = { - dsn = "https://key@host/123" - } - - local transport = transport_module.create_love2d_transport(config) - - -- Should not error - assert.has_no.errors(function() - transport:close() - end) - end) - end) - - describe("os_detection", function() - local os_detection - - before_each(function() - package.loaded["sentry.platforms.love2d.os_detection"] = nil - os_detection = require("sentry.platforms.love2d.os_detection") - end) - - it("should detect OS from Love2D", function() - local os_info = os_detection.detect_os() - assert.is_not_nil(os_info) - assert.equals("macOS", os_info.name) - assert.is_nil(os_info.version) -- Love2D doesn't provide version - end) - end) - - describe("context", function() - local context_module - - before_each(function() - package.loaded["sentry.platforms.love2d.context"] = nil - context_module = require("sentry.platforms.love2d.context") - end) - - it("should get Love2D context information", function() - local context = context_module.get_love2d_context() - - assert.is_table(context) - assert.equals("11.5.0.Mysterious Mysteries", context.love_version) - assert.equals("macOS", context.os) - assert.is_table(context.screen) - assert.equals(800, context.screen.width) - assert.equals(600, context.screen.height) - end) - - it("should handle missing Love2D gracefully", function() - _G.love = nil - - local context = context_module.get_love2d_context() - assert.is_table(context) - -- Should be empty but not error - end) - end) - - describe("integration", function() - it("should work with main Sentry SDK", function() - local sentry = require("sentry") - - -- Initialize with Love2D transport - sentry.init({ - dsn = "https://key@host/123", - environment = "test" - }) - - assert.is_not_nil(sentry._client) - assert.is_not_nil(sentry._client.transport) - - -- Should be able to capture events - local event_id = sentry.capture_message("Test message") - assert.is_string(event_id) - assert.not_equals("", event_id) - end) - - it("should work with logger module", function() - local sentry = require("sentry") - local logger = require("sentry.logger") - - sentry.init({ - dsn = "https://key@host/123" - }) - - logger.init({ - enable_logs = true, - max_buffer_size = 1 - }) - - -- Should not error - assert.has_no.errors(function() - logger.info("Test log from Love2D") - logger.flush() - end) - end) - end) - - describe("without Love2D", function() - before_each(function() - _G.love = nil - end) - - it("should not be available", function() - package.loaded["sentry.platforms.love2d.transport"] = nil - local transport_module = require("sentry.platforms.love2d.transport") - assert.is_false(transport_module.is_love2d_available()) - end) - - it("should return empty OS detection", function() - package.loaded["sentry.platforms.love2d.os_detection"] = nil - local os_detection = require("sentry.platforms.love2d.os_detection") - local os_info = os_detection.detect_os() - assert.is_nil(os_info) - end) - end) -end) \ No newline at end of file diff --git a/spec/platforms/love2d/main.lua b/spec/platforms/love2d/main.lua deleted file mode 100644 index de83d89..0000000 --- a/spec/platforms/love2d/main.lua +++ /dev/null @@ -1,373 +0,0 @@ --- Headless Love2D test runner for Sentry SDK --- This runs actual tests in the Love2D runtime environment - --- Add build path for modules -package.path = "../../../build/?.lua;../../../build/?/init.lua;" .. package.path - -local test_results = { - passed = 0, - failed = 0, - tests = {}, - start_time = 0 -} - -local function log(message) - print("[LOVE2D-TEST] " .. message) -end - -local function assert_equal(actual, expected, message) - if actual == expected then - test_results.passed = test_results.passed + 1 - log("✓ " .. (message or "assertion passed")) - table.insert(test_results.tests, {status = "PASS", message = message or "assertion"}) - else - test_results.failed = test_results.failed + 1 - local error_msg = string.format("Expected '%s', got '%s' - %s", tostring(expected), tostring(actual), message or "assertion failed") - log("✗ " .. error_msg) - table.insert(test_results.tests, {status = "FAIL", message = error_msg}) - end -end - -local function assert_true(condition, message) - assert_equal(condition, true, message) -end - -local function assert_false(condition, message) - assert_equal(condition, false, message) -end - -local function assert_not_equal(actual, expected, message) - if actual == expected then - test_results.failed = test_results.failed + 1 - local error_msg = string.format("Expected '%s' to NOT equal '%s' - %s", tostring(actual), tostring(expected), message or "assertion failed") - log("✗ " .. error_msg) - table.insert(test_results.tests, {status = "FAIL", message = error_msg}) - else - test_results.passed = test_results.passed + 1 - log("✓ " .. (message or "values are not equal")) - table.insert(test_results.tests, {status = "PASS", message = message or "values are not equal"}) - end -end - -local function assert_not_nil(value, message) - if value ~= nil then - test_results.passed = test_results.passed + 1 - log("✓ " .. (message or "value is not nil")) - table.insert(test_results.tests, {status = "PASS", message = message or "not nil"}) - else - test_results.failed = test_results.failed + 1 - local error_msg = "Expected non-nil value - " .. (message or "nil check failed") - log("✗ " .. error_msg) - table.insert(test_results.tests, {status = "FAIL", message = error_msg}) - end -end - -local function run_test(name, test_func) - log("Running test: " .. name) - local success, err = pcall(test_func) - if not success then - test_results.failed = test_results.failed + 1 - local error_msg = "Test crashed: " .. tostring(err) - log("✗ " .. error_msg) - table.insert(test_results.tests, {status = "FAIL", message = error_msg}) - end -end - -local function test_love2d_environment() - assert_not_nil(_G.love, "Love2D global should exist") - assert_not_nil(love.system, "love.system should exist") - assert_not_nil(love.thread, "love.thread should exist") - assert_not_nil(love.timer, "love.timer should exist") - - -- Test OS detection - local os_name = love.system.getOS() - assert_not_nil(os_name, "OS name should be detected") - log("Detected OS: " .. tostring(os_name)) - - -- Test Love2D version - local major, minor, revision, codename = love.getVersion() - assert_not_nil(major, "Love2D major version should exist") - local version_string = string.format("%d.%d.%d (%s)", major, minor, revision, codename) - log("Love2D version: " .. version_string) -end - -local function test_module_loading() - -- Test Sentry core module - local sentry = require("sentry") - assert_not_nil(sentry, "Sentry module should load") - assert_not_nil(sentry.init, "Sentry.init should exist") - - -- Test logger module - local logger = require("sentry.logger") - assert_not_nil(logger, "Logger module should load") - assert_not_nil(logger.init, "Logger.init should exist") - - -- Test Love2D specific modules - local transport = require("sentry.platforms.love2d.transport") - assert_not_nil(transport, "Love2D transport should load") - assert_true(transport.is_love2d_available(), "Love2D transport should be available in Love2D runtime") - - local os_detection = require("sentry.platforms.love2d.os_detection") - assert_not_nil(os_detection, "Love2D OS detection should load") - - local context = require("sentry.platforms.love2d.context") - assert_not_nil(context, "Love2D context should load") -end - -local function test_sentry_initialization() - local sentry = require("sentry") - - -- Initialize Sentry - sentry.init({ - dsn = "https://e247e6e48f8f482499052a65adaa9f6b@o117736.ingest.us.sentry.io/4504930623356928", - environment = "love2d-test", - release = "love2d-test@1.0.0", - debug = true - }) - - assert_not_nil(sentry._client, "Sentry client should be initialized") - assert_not_nil(sentry._client.transport, "Sentry transport should be initialized") - - -- Verify Love2D transport is selected - local transport_name = "unknown" - if sentry._client.transport then - -- Check if it's the Love2D transport by testing clean API methods - if sentry.flush and sentry.close then - transport_name = "love2d" - end - end - - assert_equal(transport_name, "love2d", "Love2D transport should be selected") - log("Transport selected: " .. transport_name) -end - -local function test_logger_functionality() - local logger = require("sentry.logger") - - logger.init({ - enable_logs = true, - max_buffer_size = 2, - flush_timeout = 1.0, - hook_print = false -- Disable to avoid interference with test output - }) - - -- Test basic logging - logger.info("Test log message from Love2D") - logger.error("Test error message from Love2D") - - -- Get buffer status - local status = logger.get_buffer_status() - assert_not_nil(status, "Buffer status should be available") - assert_true(status.logs >= 0, "Buffer should have non-negative log count") - - -- Test flush - logger.flush() - log("Logger test completed successfully") -end - -local function test_error_capture() - local sentry = require("sentry") - local logger = require("sentry.logger") - - -- Add breadcrumb - sentry.add_breadcrumb({ - message = "Love2D test breadcrumb", - category = "test", - level = "info" - }) - - -- Test exception capture - local function error_handler(err) - sentry.capture_exception({ - type = "Love2DTestError", - message = tostring(err) - }) - return err - end - - -- Trigger error in controlled way - local success, result = xpcall(function() - error("Love2D test error for stack trace verification") - end, error_handler) - - assert_false(success, "Error should have been caught") - assert_not_nil(result, "Error result should exist") - - log("Error capture test completed") -end - -local function test_transport_functionality() - local sentry = require("sentry") - - -- Test transport methods exist - if sentry._client and sentry._client.transport then - local transport = sentry._client.transport - - assert_not_nil(transport.flush, "Transport should have flush method") - - -- Test flush (should not error) - local success, err = pcall(function() - transport:flush() - end) - assert_true(success, "Transport flush should succeed: " .. tostring(err or "")) - - log("Transport functionality test completed") - end -end - -local function test_error_handler_integration() - log("Testing Love2D error handler integration...") - - -- Test loading integration module - local ok, integration_module = pcall(require, "sentry.platforms.love2d.integration") - assert_true(ok, "Should be able to load Love2D integration module") - assert_not_nil(integration_module.setup_love2d_integration, "Integration module should export setup function") - - -- Test creating integration instance - local integration = integration_module.setup_love2d_integration() - assert_not_nil(integration, "Integration should be created successfully") - assert_true(type(integration.install_error_handler) == "function", "Integration should have install_error_handler method") - assert_true(type(integration.uninstall_error_handler) == "function", "Integration should have uninstall_error_handler method") - - -- Store original error handler - local original_handler = love.errorhandler - - -- Test installing error handler with updated API - integration:install_error_handler({ - capture_exception = function(self, exception_data, level) - log("Mock capture_exception called with type: " .. (exception_data.type or "unknown") .. " level: " .. (level or "unknown")) - return "test-event-id" - end, - transport = { - flush = function(self) - log("Mock transport flush called") - end - } - }) - - assert_not_equal(love.errorhandler, original_handler, "Error handler should be replaced") - - -- Test the error handler by triggering a fatal error (it should be captured and re-thrown) - local error_captured = false - local flush_called = false - - integration:uninstall_error_handler() - integration:install_error_handler({ - options = { - environment = "test", - debug = true - }, - scope = { - apply_to_event = function(self, event) - log("✓ Scope applied to fatal error event") - return event - end - }, - transport = { - send = function(self, event) - log("✓ Fatal error captured with mechanism: " .. (event.exception and event.exception.values and event.exception.values[1] and event.exception.values[1].mechanism and event.exception.values[1].mechanism.type or "unknown")) - error_captured = true - if event.exception and event.exception.values and event.exception.values[1] then - local exception = event.exception.values[1] - assert_equal(exception.type, "RuntimeError", "Exception type should be RuntimeError") - assert_equal(exception.mechanism.type, "love.errorhandler", "Mechanism type should be love.errorhandler") - assert_equal(exception.mechanism.handled, false, "Mechanism handled should be false") - end - return true, nil - end, - flush = function(self) - log("✓ Transport flush called for fatal error") - flush_called = true - end - } - }) - - -- Try to trigger the error handler (should be captured then re-thrown) - local caught_error = nil - local success = pcall(function() - love.errorhandler("Fatal Love2D error triggered by test - Testing love.errorhandler integration!") - end) - - -- The error handler should have captured the error and re-thrown it - assert_true(error_captured, "Fatal error should have been captured by integration") - assert_true(flush_called, "Transport flush should have been called") - - -- Test uninstalling error handler - integration:uninstall_error_handler() - assert_equal(love.errorhandler, original_handler, "Error handler should be restored") - - log("Error handler integration test completed") -end - -function love.load() - log("Starting Love2D Sentry SDK tests...") - test_results.start_time = love.timer.getTime() - - -- Run all tests - run_test("Love2D Environment", test_love2d_environment) - run_test("Module Loading", test_module_loading) - run_test("Sentry Initialization", test_sentry_initialization) - run_test("Logger Functionality", test_logger_functionality) - run_test("Error Capture", test_error_capture) - run_test("Transport Functionality", test_transport_functionality) - run_test("Error Handler Integration", test_error_handler_integration) - - -- Wait a moment for async operations - love.timer.sleep(1) -end - -local function print_results() - local elapsed = love.timer.getTime() - test_results.start_time - - log("=== Test Results ===") - log(string.format("Tests run: %d", test_results.passed + test_results.failed)) - log(string.format("Passed: %d", test_results.passed)) - log(string.format("Failed: %d", test_results.failed)) - log(string.format("Time: %.2f seconds", elapsed)) - log("") - - -- Print individual test results - for _, test in ipairs(test_results.tests) do - log(string.format("[%s] %s", test.status, test.message)) - end - - log("") - - if test_results.failed == 0 then - log("All tests passed! 🎉") - love.event.quit(0) - else - log("Some tests failed! ❌") - love.event.quit(1) - end -end - --- Simple update loop to handle exit -local exit_timer = 0 -function love.update(dt) - exit_timer = exit_timer + dt - - -- Flush transports periodically - if love.timer.getTime() - test_results.start_time > 0.5 then - local sentry = package.loaded.sentry - if sentry and sentry.flush then - sentry.flush() - end - - local logger = package.loaded["sentry.logger"] - if logger and logger.flush then - logger.flush() - end - end - - -- Exit after enough time for async operations - if exit_timer > 3 then - print_results() - end -end - --- Minimal draw function for headless mode (graphics disabled) -function love.draw() - -- No graphics in headless mode - -- Test progress is printed to console instead -end \ No newline at end of file diff --git a/spec/platforms/lua/http_integration_spec.lua b/spec/platforms/lua/http_integration_spec.lua deleted file mode 100644 index d560d6e..0000000 --- a/spec/platforms/lua/http_integration_spec.lua +++ /dev/null @@ -1,431 +0,0 @@ -local tracing = require("sentry.tracing") -local http_client = require("platforms.lua.http_client") -local http_server = require("platforms.lua.http_server") - -describe("HTTP Integration Tests", function() - before_each(function() - tracing.clear() - end) - - after_each(function() - tracing.clear() - end) - - describe("HTTP Client Integration", function() - describe("wrap_generic_client", function() - it("should add trace headers to outgoing requests", function() - tracing.start_trace() - - local mock_client = function(url, options) - return { - url = url, - headers_sent = options.headers or {} - } - end - - local wrapped_client = http_client.wrap_generic_client(mock_client) - local result = wrapped_client("https://example.com", {method = "GET"}) - - assert.equal("https://example.com", result.url) - assert.is_not_nil(result.headers_sent["sentry-trace"]) - end) - - it("should not add headers when tracing is inactive", function() - tracing.clear() - - local mock_client = function(url, options) - return {headers_sent = options.headers or {}} - end - - local wrapped_client = http_client.wrap_generic_client(mock_client) - local result = wrapped_client("https://example.com") - - assert.is_nil(result.headers_sent["sentry-trace"]) - end) - end) - - describe("luasocket integration", function() - it("should wrap LuaSocket http module", function() - local mock_http = { - request = function(url_or_options, body) - if type(url_or_options) == "string" then - return "response", 200, {} - else - return "response", 200, url_or_options.headers or {} - end - end - } - - tracing.start_trace() - local wrapped_http = http_client.luasocket.wrap_http_module(mock_http) - - -- Test string URL format - local body, status, headers = wrapped_http.request("https://example.com") - assert.equal("response", body) - assert.equal(200, status) - - -- Test options table format - local body2, status2, headers2 = wrapped_http.request({ - url = "https://example.com", - headers = {} - }) - assert.equal("response", body2) - assert.is_not_nil(headers2["sentry-trace"]) - end) - - it("should error for invalid http module", function() - assert.has_error(function() - http_client.luasocket.wrap_http_module({}) - end) - end) - end) - - describe("lua-http integration", function() - it("should wrap lua-http request object", function() - local headers_added = {} - - local mock_request = { - get_headers_as_sequence = function() return {} end, - get_uri = function() return "https://example.com" end, - append_header = function(self, key, value) - headers_added[key] = value - end, - go = function(self) - return "response" - end - } - - tracing.start_trace() - local wrapped_request = http_client.lua_http.wrap_request(mock_request) - local result = wrapped_request:go() - - assert.equal("response", result) - assert.is_not_nil(headers_added["sentry-trace"]) - end) - - it("should error for invalid request object", function() - assert.has_error(function() - http_client.lua_http.wrap_request({}) - end, "Invalid lua-http request object") - end) - end) - - describe("create_traced_request", function() - it("should create traced request function", function() - local mock_request = function(url, options) - return { - url = url, - headers = options and options.headers or {} - } - end - - tracing.start_trace() - local traced_request = http_client.create_traced_request(mock_request) - local result = traced_request("https://example.com", {method = "GET"}) - - assert.equal("https://example.com", result.url) - assert.is_not_nil(result.headers["sentry-trace"]) - end) - - it("should error for non-function input", function() - assert.has_error(function() - http_client.create_traced_request("not a function") - end, "make_request must be a function") - end) - end) - - describe("create_middleware", function() - it("should create middleware that adds trace headers", function() - tracing.start_trace() - - local original_function = function(url, options) - return { - url = url, - headers = options and options.headers or {} - } - end - - local middleware = http_client.create_middleware(original_function) - local result = middleware("https://example.com", {headers = {}}) - - assert.equal("https://example.com", result.url) - assert.is_not_nil(result.headers["sentry-trace"]) - end) - - it("should use custom URL extractor", function() - tracing.start_trace() - - local original_function = function(config) - return { - url = config.target, - headers = config.headers or {} - } - end - - local extract_url = function(args) - return args[1].target - end - - local middleware = http_client.create_middleware(original_function, extract_url) - local result = middleware({ - target = "https://example.com", - headers = {} - }) - - assert.is_not_nil(result.headers["sentry-trace"]) - end) - - it("should error for non-function client", function() - assert.has_error(function() - http_client.create_middleware("not a function") - end, "client_function must be a function") - end) - end) - end) - - describe("HTTP Server Integration", function() - describe("wrap_handler", function() - it("should continue trace from request headers", function() - local trace_info_captured = nil - - local handler = function(request, response) - trace_info_captured = tracing.get_current_trace_info() - return "handled" - end - - local wrapped_handler = http_server.wrap_handler(handler) - - local mock_request = { - headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - local result = wrapped_handler(mock_request, {}) - - assert.equal("handled", result) - assert.is_not_nil(trace_info_captured) - assert.equal("1234567890abcdef1234567890abcdef", trace_info_captured.trace_id) - end) - - it("should start new trace when no headers present", function() - local trace_info_captured = nil - - local handler = function(request, response) - trace_info_captured = tracing.get_current_trace_info() - end - - local wrapped_handler = http_server.wrap_handler(handler) - wrapped_handler({headers = {}}, {}) - - assert.is_not_nil(trace_info_captured) - assert.is_not_nil(trace_info_captured.trace_id) - assert.is_nil(trace_info_captured.parent_span_id) -- New trace - end) - - it("should use custom header extractor", function() - local trace_info_captured = nil - - local handler = function(request, response) - trace_info_captured = tracing.get_current_trace_info() - end - - local extract_headers = function(request) - return request.custom_headers or {} - end - - local wrapped_handler = http_server.wrap_handler(handler, extract_headers) - - local mock_request = { - custom_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - wrapped_handler(mock_request, {}) - - assert.equal("1234567890abcdef1234567890abcdef", trace_info_captured.trace_id) - end) - - it("should error for non-function handler", function() - assert.has_error(function() - http_server.wrap_handler("not a function") - end, "handler must be a function") - end) - end) - - describe("create_generic_middleware", function() - it("should create middleware that continues traces", function() - local trace_info_captured = nil - - local next_func = function() - trace_info_captured = tracing.get_current_trace_info() - return "next called" - end - - local extract_headers = function(request) - return request.headers - end - - local middleware = http_server.create_generic_middleware(extract_headers) - - local mock_request = { - headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - local result = middleware(mock_request, {}, next_func) - - assert.equal("next called", result) - assert.equal("1234567890abcdef1234567890abcdef", trace_info_captured.trace_id) - end) - - it("should normalize header keys to lowercase", function() - local trace_info_captured = nil - - local next_func = function() - trace_info_captured = tracing.get_current_trace_info() - end - - local extract_headers = function(request) - return request.headers - end - - local middleware = http_server.create_generic_middleware(extract_headers) - - local mock_request = { - headers = { - ["SENTRY-TRACE"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - middleware(mock_request, {}, next_func) - - assert.equal("1234567890abcdef1234567890abcdef", trace_info_captured.trace_id) - end) - - it("should error for non-function extract_headers", function() - assert.has_error(function() - http_server.create_generic_middleware("not a function") - end, "extract_headers must be a function") - end) - end) - - describe("pegasus integration", function() - it("should wrap pegasus server start method", function() - local handler_called_with = nil - - local mock_server = { - start = function(self, handler) - handler_called_with = handler - return "server started" - end - } - - local user_handler = function(request, response) - return "user handler result" - end - - local wrapped_server = http_server.pegasus.wrap_server(mock_server) - local result = wrapped_server:start(user_handler) - - assert.equal("server started", result) - assert.is_not_nil(handler_called_with) - assert.is_function(handler_called_with) - assert.is_not_equal(user_handler, handler_called_with) -- Should be wrapped - end) - - it("should create middleware for pegasus", function() - local trace_info_captured = nil - - local middleware = http_server.pegasus.create_middleware() - - local next_func = function() - trace_info_captured = tracing.get_current_trace_info() - end - - local mock_request = { - headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - middleware(mock_request, {}, next_func) - - assert.equal("1234567890abcdef1234567890abcdef", trace_info_captured.trace_id) - end) - end) - end) - - describe("End-to-End Integration", function() - it("should propagate trace from server to client", function() - -- Simulate incoming request with trace - local incoming_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1", - ["baggage"] = "key1=value1,key2=value2" - } - - -- Continue trace in server - tracing.continue_trace_from_request(incoming_headers) - - -- Make outgoing request from server - local outgoing_headers = tracing.get_request_headers("https://downstream.com") - - -- Verify trace propagation - assert.is_not_nil(outgoing_headers["sentry-trace"]) - assert.is_not_nil(outgoing_headers["baggage"]) - - -- Parse outgoing trace header - local trace_data = tracing.headers.parse_sentry_trace(outgoing_headers["sentry-trace"]) - - -- Should have same trace ID but different span ID - assert.equal("1234567890abcdef1234567890abcdef", trace_data.trace_id) - assert.is_not_equal("abcdef1234567890", trace_data.span_id) -- New span ID - assert.equal(true, trace_data.sampled) -- Preserve sampling decision - end) - - it("should create complete trace context for events", function() - -- Start with incoming trace - local incoming_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - - tracing.continue_trace_from_request(incoming_headers) - - -- Create error event - local event = { - type = "error", - message = "Something went wrong" - } - - -- Attach trace context - local event_with_trace = tracing.attach_trace_context_to_event(event) - - -- Verify trace context - assert.is_not_nil(event_with_trace.contexts) - assert.is_not_nil(event_with_trace.contexts.trace) - assert.equal("1234567890abcdef1234567890abcdef", event_with_trace.contexts.trace.trace_id) - assert.equal("abcdef1234567890", event_with_trace.contexts.trace.parent_span_id) - assert.is_not_nil(event_with_trace.contexts.trace.span_id) - end) - - it("should handle TwP mode correctly", function() - -- Start new trace (TwP mode - no spans, just trace propagation) - tracing.start_trace() - - -- Get headers for outgoing request - local headers = tracing.get_request_headers() - - -- Parse the trace header - local trace_data = tracing.headers.parse_sentry_trace(headers["sentry-trace"]) - - -- In TwP mode, sampled should be nil (deferred) - assert.is_nil(trace_data.sampled) - - -- Should still propagate trace ID and span ID - assert.is_not_nil(trace_data.trace_id) - assert.is_not_nil(trace_data.span_id) - end) - end) -end) \ No newline at end of file diff --git a/spec/sentry_spec.lua b/spec/sentry_spec.lua deleted file mode 100644 index 1b01d88..0000000 --- a/spec/sentry_spec.lua +++ /dev/null @@ -1,116 +0,0 @@ -describe("Sentry SDK", function() - local sentry - - before_each(function() - sentry = require("sentry.init") - end) - - describe("initialization", function() - it("should require a DSN", function() - assert.has_error(function() - sentry.init({}) - end, "Sentry DSN is required") - end) - - it("should initialize with valid config", function() - local client = sentry.init({ - dsn = "https://test@sentry.io/123456", - environment = "test", - test_transport = true - }) - - assert.is_not_nil(client) - end) - end) - - describe("capture_message", function() - before_each(function() - sentry.init({ - dsn = "https://test@sentry.io/123456", - debug = true, - test_transport = true - }) - end) - - it("should capture a simple message", function() - local event_id = sentry.capture_message("Test message") - assert.is_string(event_id) - end) - - it("should capture message with level", function() - local event_id = sentry.capture_message("Warning message", "warning") - assert.is_string(event_id) - end) - - it("should fail when not initialized", function() - sentry.close() - assert.has_error(function() - sentry.capture_message("Test") - end, "Sentry not initialized. Call sentry.init() first.") - end) - end) - - describe("context management", function() - before_each(function() - sentry.init({ - dsn = "https://test@sentry.io/123456", - test_transport = true - }) - end) - - it("should set user context", function() - sentry.set_user({id = "123", email = "test@example.com"}) - - -- Capture message should succeed - local event_id = sentry.capture_message("Test user context") - assert.is_not_nil(event_id) - end) - - it("should set tags", function() - sentry.set_tag("environment", "test") - sentry.set_tag("version", "0.0.6") - - -- Capture message should succeed - local event_id = sentry.capture_message("Test tags") - assert.is_not_nil(event_id) - end) - - it("should add breadcrumbs", function() - sentry.add_breadcrumb({ - message = "User clicked button", - category = "ui", - level = "info" - }) - - -- Capture message should succeed - local event_id = sentry.capture_message("Test breadcrumbs") - assert.is_not_nil(event_id) - end) - end) - - describe("with_scope", function() - before_each(function() - sentry.init({ - dsn = "https://test@sentry.io/123456", - test_transport = true - }) - end) - - it("should isolate scope changes", function() - sentry.set_tag("global", "value") - - local scoped_event_id - sentry.with_scope(function(scope) - scope:set_tag("scoped", "temporary") - scoped_event_id = sentry.capture_message("Scoped message") - end) - - -- Verify scoped message was captured - assert.is_not_nil(scoped_event_id) - - -- Capture another message outside scope - local global_event_id = sentry.capture_message("Global message") - assert.is_not_nil(global_event_id) - end) - end) -end) \ No newline at end of file diff --git a/spec/sentry_trace_headers_spec.lua b/spec/sentry_trace_headers_spec.lua deleted file mode 100644 index dd927f2..0000000 --- a/spec/sentry_trace_headers_spec.lua +++ /dev/null @@ -1,276 +0,0 @@ --- Tests for Sentry trace header parsing and generation --- Based on Sentry distributed tracing specification - -local headers = require("sentry.tracing.headers") - -describe("sentry-trace header", function() - describe("parsing", function() - it("should parse valid header with trace_id and span_id only", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000" - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - assert.is_nil(result.sampled) - end) - - it("should parse valid header with sampled=1", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1" - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - assert.is_true(result.sampled) - end) - - it("should parse valid header with sampled=0", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0" - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - assert.is_false(result.sampled) - end) - - it("should normalize trace_id and span_id to lowercase", function() - local header = "75302AC48A024BDE9A3B3734A82E36C8-1000000000000000-1" - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - end) - - it("should handle whitespace around header value", function() - local header = " 75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1 " - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - assert.is_true(result.sampled) - end) - - it("should ignore invalid sampled values and defer sampling", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-invalid" - local result = headers.parse_sentry_trace(header) - - assert.is_not_nil(result) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", result.trace_id) - assert.are.equal("1000000000000000", result.span_id) - assert.is_nil(result.sampled) -- Deferred sampling - end) - - -- Invalid header tests - it("should return nil for nil input", function() - local result = headers.parse_sentry_trace(nil) - assert.is_nil(result) - end) - - it("should return nil for empty string", function() - local result = headers.parse_sentry_trace("") - assert.is_nil(result) - end) - - it("should return nil for whitespace only", function() - local result = headers.parse_sentry_trace(" ") - assert.is_nil(result) - end) - - it("should return nil for non-string input", function() - local result = headers.parse_sentry_trace(123) - assert.is_nil(result) - end) - - it("should return nil for header with missing span_id", function() - local header = "75302ac48a024bde9a3b3734a82e36c8" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for trace_id that is too short", function() - local header = "75302ac48a024bde-1000000000000000" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for trace_id that is too long", function() - local header = "75302ac48a024bde9a3b3734a82e36c8ab-1000000000000000" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for span_id that is too short", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-10000000000000" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for span_id that is too long", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000ab" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for non-hex characters in trace_id", function() - local header = "75302ac48a024bdg9a3b3734a82e36c8-1000000000000000" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - - it("should return nil for non-hex characters in span_id", function() - local header = "75302ac48a024bde9a3b3734a82e36c8-1000000000000g00" - local result = headers.parse_sentry_trace(header) - assert.is_nil(result) - end) - end) - - describe("generation", function() - it("should generate valid sentry-trace header without sampled flag", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000000", result) - end) - - it("should generate valid sentry-trace header with sampled=true", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - sampled = true - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1", result) - end) - - it("should generate valid sentry-trace header with sampled=false", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - sampled = false - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0", result) - end) - - it("should normalize trace_id to lowercase", function() - local trace_data = { - trace_id = "75302AC48A024BDE9A3B3734A82E36C8", - span_id = "1000000000000000" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000000", result) - end) - - it("should normalize span_id to lowercase", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000ABC" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000abc", result) - end) - - it("should omit sampled flag when sampled is nil", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - sampled = nil - } - - local result = headers.generate_sentry_trace(trace_data) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8-1000000000000000", result) - end) - - -- Error cases - it("should return nil for nil input", function() - local result = headers.generate_sentry_trace(nil) - assert.is_nil(result) - end) - - it("should return nil for non-table input", function() - local result = headers.generate_sentry_trace("invalid") - assert.is_nil(result) - end) - - it("should return nil for missing trace_id", function() - local trace_data = { - span_id = "1000000000000000" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.is_nil(result) - end) - - it("should return nil for missing span_id", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.is_nil(result) - end) - - it("should return nil for invalid trace_id length", function() - local trace_data = { - trace_id = "75302ac48a024bde", - span_id = "1000000000000000" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.is_nil(result) - end) - - it("should return nil for invalid span_id length", function() - local trace_data = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000" - } - - local result = headers.generate_sentry_trace(trace_data) - assert.is_nil(result) - end) - end) - - describe("round-trip consistency", function() - it("should parse what it generates", function() - local original = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - sampled = true - } - - local header = headers.generate_sentry_trace(original) - local parsed = headers.parse_sentry_trace(header) - - assert.are.equal(original.trace_id, parsed.trace_id) - assert.are.equal(original.span_id, parsed.span_id) - assert.are.equal(original.sampled, parsed.sampled) - end) - - it("should handle deferred sampling in round-trip", function() - local original = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000" - -- sampled is nil (deferred) - } - - local header = headers.generate_sentry_trace(original) - local parsed = headers.parse_sentry_trace(header) - - assert.are.equal(original.trace_id, parsed.trace_id) - assert.are.equal(original.span_id, parsed.span_id) - assert.is_nil(parsed.sampled) - end) - end) -end) \ No newline at end of file diff --git a/spec/trace_propagation_spec.lua b/spec/trace_propagation_spec.lua deleted file mode 100644 index ebc1967..0000000 --- a/spec/trace_propagation_spec.lua +++ /dev/null @@ -1,306 +0,0 @@ --- Tests for trace propagation context management --- Tests the core logic that manages distributed tracing state - -local propagation = require("sentry.tracing.propagation") -local headers = require("sentry.tracing.headers") - -describe("trace propagation", function() - before_each(function() - -- Clear any existing context before each test - propagation.clear_context() - end) - - after_each(function() - -- Clean up after each test - propagation.clear_context() - end) - - describe("context management", function() - it("should start with no active context", function() - local context = propagation.get_current_context() - assert.is_nil(context) - end) - - it("should create new trace context", function() - local context = propagation.start_new_trace() - - assert.is_not_nil(context) - assert.is_not_nil(context.trace_id) - assert.is_not_nil(context.span_id) - assert.is_nil(context.parent_span_id) -- Root span - assert.are.equal(32, #context.trace_id) -- 128-bit as hex - assert.are.equal(16, #context.span_id) -- 64-bit as hex - end) - - it("should set and get current context", function() - local test_context = { - trace_id = "75302ac48a024bde9a3b3734a82e36c8", - span_id = "1000000000000000", - parent_span_id = nil, - sampled = true - } - - propagation.set_current_context(test_context) - local retrieved = propagation.get_current_context() - - assert.are.equal(test_context.trace_id, retrieved.trace_id) - assert.are.equal(test_context.span_id, retrieved.span_id) - assert.are.equal(test_context.sampled, retrieved.sampled) - end) - - it("should clear context", function() - propagation.start_new_trace() - assert.is_not_nil(propagation.get_current_context()) - - propagation.clear_context() - assert.is_nil(propagation.get_current_context()) - end) - - it("should report tracing as enabled when context exists", function() - assert.is_false(propagation.is_tracing_enabled()) - - propagation.start_new_trace() - assert.is_true(propagation.is_tracing_enabled()) - - propagation.clear_context() - assert.is_false(propagation.is_tracing_enabled()) - end) - end) - - describe("trace continuation from headers", function() - it("should continue trace from sentry-trace header", function() - local http_headers = { - ["sentry-trace"] = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", context.trace_id) - assert.are.equal("1000000000000000", context.parent_span_id) -- Incoming span becomes parent - assert.is_not_nil(context.span_id) -- New span ID generated - assert.is_true(context.sampled) - assert.are.not_equal("1000000000000000", context.span_id) -- Should be new span ID - end) - - it("should continue trace from W3C traceparent header", function() - local http_headers = { - ["traceparent"] = "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-01" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", context.trace_id) - assert.are.equal("1000000000000000", context.parent_span_id) - assert.is_true(context.sampled) - end) - - it("should prefer sentry-trace over traceparent", function() - local http_headers = { - ["sentry-trace"] = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1", - ["traceparent"] = "00-differenttraceid123456789abcdef0-2000000000000000-01" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", context.trace_id) - assert.are.equal("1000000000000000", context.parent_span_id) - end) - - it("should handle case-insensitive header names", function() - local http_headers = { - ["Sentry-Trace"] = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.are.equal("75302ac48a024bde9a3b3734a82e36c8", context.trace_id) - end) - - it("should parse baggage header", function() - local http_headers = { - ["sentry-trace"] = "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1", - ["baggage"] = "sentry-environment=production,sentry-release=1.0.0" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.is_not_nil(context.baggage) - assert.are.equal("production", context.baggage["sentry-environment"]) - assert.are.equal("1.0.0", context.baggage["sentry-release"]) - end) - - it("should start new trace when no valid headers present", function() - local http_headers = { - ["some-other-header"] = "value" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.is_not_nil(context.trace_id) - assert.is_not_nil(context.span_id) - assert.is_nil(context.parent_span_id) -- New root trace - end) - - it("should handle invalid sentry-trace header gracefully", function() - local http_headers = { - ["sentry-trace"] = "invalid-header-format" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context) - assert.is_not_nil(context.trace_id) - assert.is_nil(context.parent_span_id) -- Falls back to new trace - end) - end) - - describe("trace header generation", function() - it("should generate headers for outgoing requests", function() - local context = propagation.start_new_trace() - - local headers_out = propagation.get_trace_headers_for_request("http://example.com") - - assert.is_not_nil(headers_out["sentry-trace"]) - assert.is_not_nil(headers_out["sentry-trace"]:match(context.trace_id .. "%-" .. context.span_id)) - end) - - it("should include baggage in outgoing headers", function() - local context = propagation.start_new_trace({ - baggage = { - ["sentry-environment"] = "test" - } - }) - context.baggage = { ["sentry-environment"] = "test" } - propagation.set_current_context(context) - - local headers_out = propagation.get_trace_headers_for_request("http://example.com") - - assert.is_not_nil(headers_out["baggage"]) - assert.is_not_nil(headers_out["baggage"]:match("sentry%-environment=test")) - end) - - it("should return empty headers when no context", function() - -- No trace context set - local headers_out = propagation.get_trace_headers_for_request("http://example.com") - - assert.are.equal("table", type(headers_out)) - assert.is_nil(headers_out["sentry-trace"]) - end) - end) - - describe("trace propagation targeting", function() - it("should propagate to all targets with wildcard", function() - local context = propagation.start_new_trace() - local options = { - trace_propagation_targets = {"*"} - } - - local headers1 = propagation.get_trace_headers_for_request("http://example.com", options) - local headers2 = propagation.get_trace_headers_for_request("https://api.service.com", options) - - assert.is_not_nil(headers1["sentry-trace"]) - assert.is_not_nil(headers2["sentry-trace"]) - end) - - it("should propagate only to matching targets", function() - local context = propagation.start_new_trace() - local options = { - trace_propagation_targets = {"api.service.com"} - } - - local matching_headers = propagation.get_trace_headers_for_request("https://api.service.com/endpoint", options) - local non_matching_headers = propagation.get_trace_headers_for_request("https://other.com/endpoint", options) - - assert.is_not_nil(matching_headers["sentry-trace"]) - assert.is_nil(non_matching_headers["sentry-trace"]) - end) - - it("should not propagate when no targets match", function() - local context = propagation.start_new_trace() - local options = { - trace_propagation_targets = {"specific.domain.com"} - } - - local headers = propagation.get_trace_headers_for_request("https://other.domain.com", options) - - assert.is_nil(headers["sentry-trace"]) - end) - - it("should propagate to all when no targeting configured", function() - local context = propagation.start_new_trace() - local options = {} -- No trace_propagation_targets - - local headers = propagation.get_trace_headers_for_request("http://example.com", options) - - assert.is_not_nil(headers["sentry-trace"]) - end) - end) - - describe("child context creation", function() - it("should create child from existing context", function() - local parent = propagation.start_new_trace() - - local child = propagation.create_child_context() - - assert.is_not_nil(child) - assert.are.equal(parent.trace_id, child.trace_id) -- Same trace - assert.are.equal(parent.span_id, child.parent_span_id) -- Parent's span becomes parent_span_id - assert.are.not_equal(parent.span_id, child.span_id) -- New span ID - end) - - it("should start new trace when no parent context", function() - -- No existing context - local child = propagation.create_child_context() - - assert.is_not_nil(child) - assert.is_not_nil(child.trace_id) - assert.is_not_nil(child.span_id) - assert.is_nil(child.parent_span_id) -- Root trace - end) - end) - - describe("trace context for events", function() - it("should generate trace context for events", function() - local context = propagation.start_new_trace() - - local event_context = propagation.get_trace_context_for_event() - - assert.is_not_nil(event_context) - assert.are.equal(context.trace_id, event_context.trace_id) - assert.are.equal(context.span_id, event_context.span_id) - assert.are.equal(context.parent_span_id, event_context.parent_span_id) - end) - - it("should return nil when no trace context", function() - local event_context = propagation.get_trace_context_for_event() - assert.is_nil(event_context) - end) - end) - - describe("edge cases", function() - it("should handle nil headers gracefully", function() - local context = propagation.continue_trace_from_headers(nil) - assert.is_not_nil(context) -- Should create new trace - end) - - it("should handle empty headers table", function() - local context = propagation.continue_trace_from_headers({}) - assert.is_not_nil(context) -- Should create new trace - end) - - it("should generate valid trace and span IDs", function() - local context = propagation.start_new_trace() - - assert.is_not_nil(context.trace_id:match("^[0-9a-f]+$")) - assert.is_not_nil(context.span_id:match("^[0-9a-f]+$")) - assert.are.equal(32, #context.trace_id) - assert.are.equal(16, #context.span_id) - end) - end) -end) \ No newline at end of file diff --git a/spec/tracing/headers_spec.lua b/spec/tracing/headers_spec.lua deleted file mode 100644 index e38da09..0000000 --- a/spec/tracing/headers_spec.lua +++ /dev/null @@ -1,344 +0,0 @@ -local headers = require("sentry.tracing.headers") - -describe("sentry.tracing.headers", function() - describe("parse_sentry_trace", function() - it("should parse valid sentry-trace header", function() - local trace_data = headers.parse_sentry_trace("1234567890abcdef1234567890abcdef-1234567890abcdef-1") - - assert.is_not_nil(trace_data) - assert.equal("1234567890abcdef1234567890abcdef", trace_data.trace_id) - assert.equal("1234567890abcdef", trace_data.span_id) - assert.equal(true, trace_data.sampled) - end) - - it("should parse valid sentry-trace header without sampled flag", function() - local trace_data = headers.parse_sentry_trace("abcdef1234567890abcdef1234567890-abcdef1234567890") - - assert.is_not_nil(trace_data) - assert.equal("abcdef1234567890abcdef1234567890", trace_data.trace_id) - assert.equal("abcdef1234567890", trace_data.span_id) - assert.is_nil(trace_data.sampled) - end) - - it("should parse sampled=0 as false", function() - local trace_data = headers.parse_sentry_trace("1234567890abcdef1234567890abcdef-1234567890abcdef-0") - - assert.is_not_nil(trace_data) - assert.equal(false, trace_data.sampled) - end) - - it("should normalize trace_id and span_id to lowercase", function() - local trace_data = headers.parse_sentry_trace("ABCDEF1234567890ABCDEF1234567890-ABCDEF1234567890-1") - - assert.is_not_nil(trace_data) - assert.equal("abcdef1234567890abcdef1234567890", trace_data.trace_id) - assert.equal("abcdef1234567890", trace_data.span_id) - end) - - it("should handle whitespace", function() - local trace_data = headers.parse_sentry_trace(" 1234567890abcdef1234567890abcdef-1234567890abcdef-1 ") - - assert.is_not_nil(trace_data) - assert.equal("1234567890abcdef1234567890abcdef", trace_data.trace_id) - assert.equal("1234567890abcdef", trace_data.span_id) - assert.equal(true, trace_data.sampled) - end) - - it("should return nil for invalid header values", function() - assert.is_nil(headers.parse_sentry_trace(nil)) - assert.is_nil(headers.parse_sentry_trace("")) - assert.is_nil(headers.parse_sentry_trace(" ")) - assert.is_nil(headers.parse_sentry_trace("invalid-format")) - assert.is_nil(headers.parse_sentry_trace("too-short")) - assert.is_nil(headers.parse_sentry_trace("not-hex-characters-here123456789012-1234567890abcdef")) - end) - - it("should return nil for invalid trace_id length", function() - assert.is_nil(headers.parse_sentry_trace("short-1234567890abcdef-1")) - assert.is_nil(headers.parse_sentry_trace("toolong1234567890abcdef1234567890ab-1234567890abcdef-1")) - end) - - it("should return nil for invalid span_id length", function() - assert.is_nil(headers.parse_sentry_trace("1234567890abcdef1234567890abcdef-short-1")) - assert.is_nil(headers.parse_sentry_trace("1234567890abcdef1234567890abcdef-toolong567890abcdef-1")) - end) - - it("should ignore invalid sampled values", function() - local trace_data = headers.parse_sentry_trace("1234567890abcdef1234567890abcdef-1234567890abcdef-invalid") - - assert.is_not_nil(trace_data) - assert.is_nil(trace_data.sampled) -- Should defer sampling decision - end) - end) - - describe("generate_sentry_trace", function() - it("should generate valid sentry-trace header", function() - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef", - sampled = true - } - - local header = headers.generate_sentry_trace(trace_data) - assert.equal("1234567890abcdef1234567890abcdef-1234567890abcdef-1", header) - end) - - it("should generate header without sampled flag when sampled is nil", function() - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef" - } - - local header = headers.generate_sentry_trace(trace_data) - assert.equal("1234567890abcdef1234567890abcdef-1234567890abcdef", header) - end) - - it("should generate header with sampled=0 when sampled is false", function() - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef", - sampled = false - } - - local header = headers.generate_sentry_trace(trace_data) - assert.equal("1234567890abcdef1234567890abcdef-1234567890abcdef-0", header) - end) - - it("should normalize to lowercase", function() - local trace_data = { - trace_id = "ABCDEF1234567890ABCDEF1234567890", - span_id = "ABCDEF1234567890" - } - - local header = headers.generate_sentry_trace(trace_data) - assert.equal("abcdef1234567890abcdef1234567890-abcdef1234567890", header) - end) - - it("should return nil for invalid input", function() - assert.is_nil(headers.generate_sentry_trace(nil)) - assert.is_nil(headers.generate_sentry_trace({})) - assert.is_nil(headers.generate_sentry_trace({trace_id = "invalid"})) - assert.is_nil(headers.generate_sentry_trace({span_id = "invalid"})) - assert.is_nil(headers.generate_sentry_trace({ - trace_id = "short", - span_id = "1234567890abcdef" - })) - end) - end) - - describe("parse_baggage", function() - it("should parse simple baggage header", function() - local baggage = headers.parse_baggage("key1=value1,key2=value2") - - assert.equal("value1", baggage.key1) - assert.equal("value2", baggage.key2) - end) - - it("should parse baggage with properties (ignore properties)", function() - local baggage = headers.parse_baggage("key1=value1;property=prop,key2=value2") - - assert.equal("value1", baggage.key1) - assert.equal("value2", baggage.key2) - end) - - it("should handle whitespace", function() - local baggage = headers.parse_baggage(" key1=value1 , key2=value2 ") - - assert.equal("value1", baggage.key1) - assert.equal("value2", baggage.key2) - end) - - it("should return empty table for invalid input", function() - local baggage1 = headers.parse_baggage(nil) - local baggage2 = headers.parse_baggage("") - local baggage3 = headers.parse_baggage(" ") - - assert.same({}, baggage1) - assert.same({}, baggage2) - assert.same({}, baggage3) - end) - - it("should ignore invalid key-value pairs", function() - local baggage = headers.parse_baggage("key1=value1,invalid_entry,key2=value2") - - assert.equal("value1", baggage.key1) - assert.equal("value2", baggage.key2) - assert.is_nil(baggage.invalid_entry) - end) - end) - - describe("generate_baggage", function() - it("should generate valid baggage header", function() - local baggage_data = { - key1 = "value1", - key2 = "value2" - } - - local header = headers.generate_baggage(baggage_data) - - -- Order is not guaranteed, so check both possibilities - local expected1 = "key1=value1,key2=value2" - local expected2 = "key2=value2,key1=value1" - - assert.is_true(header == expected1 or header == expected2) - end) - - it("should URL encode special characters", function() - local baggage_data = { - key1 = "value,with,commas", - key2 = "value;with;semicolons", - key3 = "value=with=equals" - } - - local header = headers.generate_baggage(baggage_data) - - assert.is_true(header:find("value%%2Cwith%%2Ccommas") ~= nil) - assert.is_true(header:find("value%%3Bwith%%3Bsemicolons") ~= nil) - assert.is_true(header:find("value%%3Dwith%%3Dequals") ~= nil) - end) - - it("should return nil for empty or invalid input", function() - assert.is_nil(headers.generate_baggage(nil)) - assert.is_nil(headers.generate_baggage({})) - assert.is_nil(headers.generate_baggage({key1 = 123})) -- Non-string value - end) - end) - - describe("generate_trace_id", function() - it("should generate valid trace IDs", function() - local id1 = headers.generate_trace_id() - local id2 = headers.generate_trace_id() - - assert.equal(32, #id1) - assert.equal(32, #id2) - assert.is_not_equal(id1, id2) -- Should be unique - assert.is_true(id1:match("^[0-9a-f]+$") ~= nil) -- Should be hex - end) - end) - - describe("generate_span_id", function() - it("should generate valid span IDs", function() - local id1 = headers.generate_span_id() - local id2 = headers.generate_span_id() - - assert.equal(16, #id1) - assert.equal(16, #id2) - assert.is_not_equal(id1, id2) -- Should be unique - assert.is_true(id1:match("^[0-9a-f]+$") ~= nil) -- Should be hex - end) - end) - - describe("extract_trace_headers", function() - it("should extract sentry-trace header", function() - local http_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ["content-type"] = "application/json" - } - - local trace_info = headers.extract_trace_headers(http_headers) - - assert.is_not_nil(trace_info.sentry_trace) - assert.equal("1234567890abcdef1234567890abcdef", trace_info.sentry_trace.trace_id) - end) - - it("should extract baggage header", function() - local http_headers = { - ["baggage"] = "key1=value1,key2=value2" - } - - local trace_info = headers.extract_trace_headers(http_headers) - - assert.is_not_nil(trace_info.baggage) - assert.equal("value1", trace_info.baggage.key1) - assert.equal("value2", trace_info.baggage.key2) - end) - - it("should be case-insensitive for header names", function() - local http_headers = { - ["SENTRY-TRACE"] = "1234567890abcdef1234567890abcdef-1234567890abcdef-1", - ["Baggage"] = "key1=value1" - } - - local trace_info = headers.extract_trace_headers(http_headers) - - assert.is_not_nil(trace_info.sentry_trace) - assert.is_not_nil(trace_info.baggage) - end) - - it("should extract traceparent header", function() - local http_headers = { - ["traceparent"] = "00-1234567890abcdef1234567890abcdef-1234567890abcdef-01" - } - - local trace_info = headers.extract_trace_headers(http_headers) - - assert.equal("00-1234567890abcdef1234567890abcdef-1234567890abcdef-01", trace_info.traceparent) - end) - - it("should return empty table for invalid input", function() - local trace_info1 = headers.extract_trace_headers(nil) - local trace_info2 = headers.extract_trace_headers({}) - - assert.same({}, trace_info1) - assert.same({}, trace_info2) - end) - end) - - describe("inject_trace_headers", function() - it("should inject sentry-trace header", function() - local http_headers = {} - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef", - sampled = true - } - - headers.inject_trace_headers(http_headers, trace_data) - - assert.equal("1234567890abcdef1234567890abcdef-1234567890abcdef-1", http_headers["sentry-trace"]) - end) - - it("should inject baggage header", function() - local http_headers = {} - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef" - } - local baggage_data = { - key1 = "value1", - key2 = "value2" - } - - headers.inject_trace_headers(http_headers, trace_data, baggage_data) - - assert.is_not_nil(http_headers["baggage"]) - assert.is_true(http_headers["baggage"]:find("key1=value1") ~= nil) - assert.is_true(http_headers["baggage"]:find("key2=value2") ~= nil) - end) - - it("should inject traceparent header when requested", function() - local http_headers = {} - local trace_data = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "1234567890abcdef", - sampled = true - } - local options = { - include_traceparent = true - } - - headers.inject_trace_headers(http_headers, trace_data, nil, options) - - assert.equal("00-1234567890abcdef1234567890abcdef-1234567890abcdef-01", http_headers["traceparent"]) - end) - - it("should not modify headers for invalid input", function() - local http_headers = {existing = "value"} - - headers.inject_trace_headers(http_headers, nil) - - assert.equal("value", http_headers.existing) - assert.is_nil(http_headers["sentry-trace"]) - end) - end) -end) \ No newline at end of file diff --git a/spec/tracing/init_spec.lua b/spec/tracing/init_spec.lua deleted file mode 100644 index 5548173..0000000 --- a/spec/tracing/init_spec.lua +++ /dev/null @@ -1,378 +0,0 @@ -local tracing = require("sentry.tracing") - -describe("sentry.tracing", function() - before_each(function() - -- Clear any existing context before each test - tracing.clear() - end) - - after_each(function() - -- Clean up after each test - tracing.clear() - end) - - describe("init", function() - it("should initialize tracing with default config", function() - tracing.init() - - assert.equal(true, tracing.is_active()) - end) - - it("should initialize tracing with custom config", function() - local config = { - trace_propagation_targets = {"example%.com"}, - include_traceparent = true - } - - tracing.init(config) - - assert.equal(true, tracing.is_active()) - assert.same(config, tracing._config) - end) - end) - - describe("continue_trace_from_request", function() - it("should continue trace from request headers", function() - local request_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - - local trace_context = tracing.continue_trace_from_request(request_headers) - - assert.is_not_nil(trace_context) - assert.equal("1234567890abcdef1234567890abcdef", trace_context.trace_id) - assert.equal("abcdef1234567890", trace_context.parent_span_id) - end) - - it("should start new trace when no headers", function() - local request_headers = {} - - local trace_context = tracing.continue_trace_from_request(request_headers) - - assert.is_not_nil(trace_context) - assert.is_not_nil(trace_context.trace_id) - assert.is_nil(trace_context.parent_span_id) - end) - end) - - describe("get_request_headers", function() - it("should return empty table when not active", function() - tracing.clear() - local headers = tracing.get_request_headers() - assert.same({}, headers) - end) - - it("should return sentry-trace header when active", function() - tracing.start_trace() - - local headers = tracing.get_request_headers() - - assert.is_not_nil(headers["sentry-trace"]) - end) - - it("should use config for traceparent inclusion", function() - tracing.init({include_traceparent = true}) - tracing.start_trace() - - local headers = tracing.get_request_headers() - - assert.is_not_nil(headers["sentry-trace"]) - assert.is_not_nil(headers["traceparent"]) - end) - - it("should respect trace propagation targets from config", function() - tracing.init({trace_propagation_targets = {"^https://example%.com"}}) - tracing.start_trace() - - local headers1 = tracing.get_request_headers("https://other.com") - local headers2 = tracing.get_request_headers("https://example.com") - - assert.same({}, headers1) -- Should not propagate - assert.is_not_nil(headers2["sentry-trace"]) -- Should propagate - end) - end) - - describe("start_trace", function() - it("should start new trace", function() - local trace_context = tracing.start_trace() - - assert.is_not_nil(trace_context) - assert.is_not_nil(trace_context.trace_id) - assert.is_not_nil(trace_context.span_id) - assert.is_nil(trace_context.parent_span_id) - end) - - it("should include options", function() - local options = { - baggage = {key1 = "value1"} - } - - local trace_context = tracing.start_trace(options) - - -- Note: baggage is stored in propagation context, not returned in trace_context - assert.is_not_nil(trace_context) - end) - end) - - describe("create_child", function() - it("should create child span context", function() - tracing.start_trace() - local parent_info = tracing.get_current_trace_info() - - local child_context = tracing.create_child() - - assert.equal(parent_info.trace_id, child_context.trace_id) - assert.equal(parent_info.span_id, child_context.parent_span_id) - assert.is_not_equal(parent_info.span_id, child_context.span_id) - end) - - it("should start new trace when no parent", function() - local child_context = tracing.create_child() - - assert.is_not_nil(child_context.trace_id) - assert.is_nil(child_context.parent_span_id) - end) - end) - - describe("get_current_trace_info", function() - it("should return nil when not active", function() - tracing.clear() - - local trace_info = tracing.get_current_trace_info() - assert.is_nil(trace_info) - end) - - it("should return trace info when active", function() - tracing.start_trace() - - local trace_info = tracing.get_current_trace_info() - - assert.is_not_nil(trace_info) - assert.is_not_nil(trace_info.trace_id) - assert.is_not_nil(trace_info.span_id) - assert.equal(true, trace_info.is_tracing_enabled) - end) - end) - - describe("is_active", function() - it("should return false when not active", function() - tracing.clear() - assert.equal(false, tracing.is_active()) - end) - - it("should return true when active", function() - tracing.start_trace() - assert.equal(true, tracing.is_active()) - end) - end) - - describe("attach_trace_context_to_event", function() - it("should not modify event when not active", function() - tracing.clear() - local event = {type = "error", message = "test"} - - local modified_event = tracing.attach_trace_context_to_event(event) - - assert.equal(event, modified_event) - assert.is_nil(modified_event.contexts) - end) - - it("should attach trace context when active", function() - tracing.start_trace() - local event = {type = "error", message = "test"} - - local modified_event = tracing.attach_trace_context_to_event(event) - - assert.equal(event, modified_event) - assert.is_not_nil(modified_event.contexts) - assert.is_not_nil(modified_event.contexts.trace) - assert.is_not_nil(modified_event.contexts.trace.trace_id) - end) - - it("should preserve existing contexts", function() - tracing.start_trace() - local event = { - type = "error", - message = "test", - contexts = { - runtime = {name = "lua", version = "5.4"} - } - } - - local modified_event = tracing.attach_trace_context_to_event(event) - - assert.is_not_nil(modified_event.contexts.runtime) - assert.is_not_nil(modified_event.contexts.trace) - end) - - it("should handle invalid input", function() - local result1 = tracing.attach_trace_context_to_event(nil) - local result2 = tracing.attach_trace_context_to_event("not a table") - - assert.is_nil(result1) - assert.equal("not a table", result2) - end) - end) - - describe("wrap_http_request", function() - it("should wrap HTTP request function with trace headers", function() - tracing.start_trace() - - local mock_client = function(url, options) - return { - url = url, - headers = options.headers or {} - } - end - - local result = tracing.wrap_http_request(mock_client, "https://example.com", { - method = "GET" - }) - - assert.equal("https://example.com", result.url) - assert.is_not_nil(result.headers["sentry-trace"]) - end) - - it("should merge with existing headers", function() - tracing.start_trace() - - local mock_client = function(url, options) - return options.headers - end - - local result = tracing.wrap_http_request(mock_client, "https://example.com", { - headers = { - ["content-type"] = "application/json" - } - }) - - assert.equal("application/json", result["content-type"]) - assert.is_not_nil(result["sentry-trace"]) - end) - - it("should error for non-function client", function() - assert.has_error(function() - tracing.wrap_http_request("not a function", "https://example.com") - end, "http_client must be a function") - end) - end) - - describe("wrap_http_handler", function() - it("should wrap handler with trace continuation", function() - local trace_id_captured = nil - - local mock_handler = function(request, response) - trace_id_captured = tracing.get_current_trace_info() - return "handled" - end - - local wrapped_handler = tracing.wrap_http_handler(mock_handler) - - local mock_request = { - headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - } - - local result = wrapped_handler(mock_request, {}) - - assert.equal("handled", result) - assert.is_not_nil(trace_id_captured) - assert.equal("1234567890abcdef1234567890abcdef", trace_id_captured.trace_id) - end) - - it("should handle requests without trace headers", function() - local trace_info_captured = nil - - local mock_handler = function(request, response) - trace_info_captured = tracing.get_current_trace_info() - end - - local wrapped_handler = tracing.wrap_http_handler(mock_handler) - wrapped_handler({headers = {}}, {}) - - assert.is_not_nil(trace_info_captured) -- Should have started new trace - end) - - it("should handle requests with get_header method", function() - local trace_id_captured = nil - - local mock_handler = function(request, response) - trace_id_captured = tracing.get_current_trace_info() - end - - local wrapped_handler = tracing.wrap_http_handler(mock_handler) - - local mock_request = { - get_header = function(self, name) - if name == "sentry-trace" then - return "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - end - return nil - end - } - - wrapped_handler(mock_request, {}) - - assert.is_not_nil(trace_id_captured) - assert.equal("1234567890abcdef1234567890abcdef", trace_id_captured.trace_id) - end) - - it("should propagate errors from handler", function() - local mock_handler = function(request, response) - error("handler error") - end - - local wrapped_handler = tracing.wrap_http_handler(mock_handler) - - assert.has_error(function() - wrapped_handler({headers = {}}, {}) - end) - end) - - it("should error for non-function handler", function() - assert.has_error(function() - tracing.wrap_http_handler("not a function") - end, "handler must be a function") - end) - end) - - describe("generate_ids", function() - it("should generate valid trace and span IDs", function() - local ids = tracing.generate_ids() - - assert.is_not_nil(ids.trace_id) - assert.is_not_nil(ids.span_id) - assert.equal(32, #ids.trace_id) -- 128-bit as hex - assert.equal(16, #ids.span_id) -- 64-bit as hex - assert.is_true(ids.trace_id:match("^[0-9a-f]+$") ~= nil) - assert.is_true(ids.span_id:match("^[0-9a-f]+$") ~= nil) - end) - - it("should generate unique IDs", function() - local ids1 = tracing.generate_ids() - local ids2 = tracing.generate_ids() - - assert.is_not_equal(ids1.trace_id, ids2.trace_id) - assert.is_not_equal(ids1.span_id, ids2.span_id) - end) - end) - - describe("get_envelope_trace_header", function() - it("should return nil when not active", function() - tracing.clear() - - local header = tracing.get_envelope_trace_header() - assert.is_nil(header) - end) - - it("should return DSC when active", function() - tracing.start_trace() - - local header = tracing.get_envelope_trace_header() - - assert.is_not_nil(header) - assert.is_not_nil(header["sentry-trace_id"]) - end) - end) -end) \ No newline at end of file diff --git a/spec/tracing/propagation_spec.lua b/spec/tracing/propagation_spec.lua deleted file mode 100644 index bdc7967..0000000 --- a/spec/tracing/propagation_spec.lua +++ /dev/null @@ -1,361 +0,0 @@ -local propagation = require("sentry.tracing.propagation") - -describe("sentry.tracing.propagation", function() - before_each(function() - -- Clear any existing context before each test - propagation.clear_context() - end) - - after_each(function() - -- Clean up after each test - propagation.clear_context() - end) - - describe("create_context", function() - it("should create new context without incoming trace data", function() - local context = propagation.create_context() - - assert.is_not_nil(context.trace_id) - assert.is_not_nil(context.span_id) - assert.is_nil(context.parent_span_id) - assert.is_nil(context.sampled) -- Deferred sampling in TwP mode - assert.same({}, context.baggage) - end) - - it("should continue context from incoming trace data", function() - local incoming_trace = { - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890", - sampled = true - } - - local context = propagation.create_context(incoming_trace) - - assert.equal("1234567890abcdef1234567890abcdef", context.trace_id) - assert.equal("abcdef1234567890", context.parent_span_id) -- Incoming span becomes parent - assert.is_not_nil(context.span_id) -- New span ID generated - assert.is_not_equal("abcdef1234567890", context.span_id) -- Should be different - assert.equal(true, context.sampled) - end) - - it("should include baggage data", function() - local baggage_data = { - key1 = "value1", - key2 = "value2" - } - - local context = propagation.create_context(nil, baggage_data) - - assert.equal("value1", context.baggage.key1) - assert.equal("value2", context.baggage.key2) - end) - end) - - describe("get_current_context and set_current_context", function() - it("should return nil when no context is set", function() - assert.is_nil(propagation.get_current_context()) - end) - - it("should store and retrieve context", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local retrieved = propagation.get_current_context() - assert.equal(context, retrieved) - end) - - it("should clear context", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - propagation.clear_context() - assert.is_nil(propagation.get_current_context()) - end) - end) - - describe("continue_trace_from_headers", function() - it("should continue trace from sentry-trace header", function() - local http_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.equal("1234567890abcdef1234567890abcdef", context.trace_id) - assert.equal("abcdef1234567890", context.parent_span_id) - assert.equal(true, context.sampled) - - -- Should also set as current context - assert.equal(context, propagation.get_current_context()) - end) - - it("should continue trace from W3C traceparent header", function() - local http_headers = { - ["traceparent"] = "00-1234567890abcdef1234567890abcdef-abcdef1234567890-01" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.equal("1234567890abcdef1234567890abcdef", context.trace_id) - assert.equal("abcdef1234567890", context.parent_span_id) - assert.equal(true, context.sampled) - end) - - it("should prioritize sentry-trace over traceparent", function() - local http_headers = { - ["sentry-trace"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-0", - ["traceparent"] = "00-1234567890abcdef1234567890abcdef-abcdef1234567890-01" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - -- Should use sentry-trace values - assert.equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", context.trace_id) - assert.equal("bbbbbbbbbbbbbbbb", context.parent_span_id) - assert.equal(false, context.sampled) - end) - - it("should include baggage data", function() - local http_headers = { - ["sentry-trace"] = "1234567890abcdef1234567890abcdef-abcdef1234567890-1", - ["baggage"] = "key1=value1,key2=value2" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.equal("value1", context.baggage.key1) - assert.equal("value2", context.baggage.key2) - end) - - it("should start new trace when no trace headers present", function() - local http_headers = { - ["content-type"] = "application/json" - } - - local context = propagation.continue_trace_from_headers(http_headers) - - assert.is_not_nil(context.trace_id) - assert.is_not_nil(context.span_id) - assert.is_nil(context.parent_span_id) - assert.is_nil(context.sampled) -- Deferred sampling - end) - end) - - describe("get_trace_headers_for_request", function() - it("should return empty table when no current context", function() - local headers = propagation.get_trace_headers_for_request() - assert.same({}, headers) - end) - - it("should generate sentry-trace header", function() - local context = propagation.create_context({ - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890", - sampled = true - }) - propagation.set_current_context(context) - - local headers = propagation.get_trace_headers_for_request() - - assert.is_not_nil(headers["sentry-trace"]) - assert.is_true(headers["sentry-trace"]:find("1234567890abcdef1234567890abcdef") ~= nil) - end) - - it("should include baggage header when baggage exists", function() - local context = propagation.create_context(nil, { - key1 = "value1", - key2 = "value2" - }) - propagation.set_current_context(context) - - local headers = propagation.get_trace_headers_for_request() - - assert.is_not_nil(headers["baggage"]) - assert.is_true(headers["baggage"]:find("key1=value1") ~= nil) - end) - - it("should include traceparent when requested", function() - local context = propagation.create_context({ - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890", - sampled = true - }) - propagation.set_current_context(context) - - local headers = propagation.get_trace_headers_for_request(nil, { - include_traceparent = true - }) - - assert.is_not_nil(headers["traceparent"]) - assert.is_true(headers["traceparent"]:find("1234567890abcdef1234567890abcdef") ~= nil) - end) - - it("should respect trace propagation targets", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local headers = propagation.get_trace_headers_for_request("https://example.com", { - trace_propagation_targets = {"notexample%.com"} - }) - - assert.same({}, headers) -- Should not propagate to non-matching targets - end) - - it("should propagate to matching targets", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local headers = propagation.get_trace_headers_for_request("https://example.com", { - trace_propagation_targets = {"example%.com"} - }) - - assert.is_not_nil(headers["sentry-trace"]) -- Should propagate to matching targets - end) - end) - - describe("get_trace_context_for_event", function() - it("should return nil when no context", function() - local trace_context = propagation.get_trace_context_for_event() - assert.is_nil(trace_context) - end) - - it("should return trace context for events", function() - local context = propagation.create_context({ - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890", - sampled = true - }) - propagation.set_current_context(context) - - local trace_context = propagation.get_trace_context_for_event() - - assert.equal("1234567890abcdef1234567890abcdef", trace_context.trace_id) - assert.equal("abcdef1234567890", trace_context.parent_span_id) -- Parent from incoming - assert.is_not_nil(trace_context.span_id) - end) - end) - - describe("start_new_trace", function() - it("should start new trace and set as current", function() - local context = propagation.start_new_trace() - - assert.is_not_nil(context.trace_id) - assert.is_not_nil(context.span_id) - assert.is_nil(context.parent_span_id) - assert.is_nil(context.sampled) -- Deferred sampling - - assert.equal(context, propagation.get_current_context()) - end) - - it("should include baggage options", function() - local options = { - baggage = { - key1 = "value1" - } - } - - local context = propagation.start_new_trace(options) - - assert.equal("value1", context.baggage.key1) - end) - end) - - describe("create_child_context", function() - it("should create child from current context", function() - local parent_context = propagation.create_context({ - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890", - sampled = true - }) - propagation.set_current_context(parent_context) - - local child_context = propagation.create_child_context() - - assert.equal(parent_context.trace_id, child_context.trace_id) -- Same trace ID - assert.equal(parent_context.span_id, child_context.parent_span_id) -- Parent span ID - assert.is_not_equal(parent_context.span_id, child_context.span_id) -- Different span ID - assert.equal(parent_context.sampled, child_context.sampled) -- Same sampling decision - end) - - it("should start new trace when no parent context", function() - local child_context = propagation.create_child_context() - - assert.is_not_nil(child_context.trace_id) - assert.is_not_nil(child_context.span_id) - assert.is_nil(child_context.parent_span_id) - end) - end) - - describe("utility functions", function() - describe("is_tracing_enabled", function() - it("should return false when no context", function() - assert.equal(false, propagation.is_tracing_enabled()) - end) - - it("should return true when context exists", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - assert.equal(true, propagation.is_tracing_enabled()) - end) - end) - - describe("get_current_trace_id", function() - it("should return nil when no context", function() - assert.is_nil(propagation.get_current_trace_id()) - end) - - it("should return trace ID when context exists", function() - local context = propagation.create_context({ - trace_id = "1234567890abcdef1234567890abcdef", - span_id = "abcdef1234567890" - }) - propagation.set_current_context(context) - - assert.equal("1234567890abcdef1234567890abcdef", propagation.get_current_trace_id()) - end) - end) - - describe("get_current_span_id", function() - it("should return nil when no context", function() - assert.is_nil(propagation.get_current_span_id()) - end) - - it("should return span ID when context exists", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local span_id = propagation.get_current_span_id() - assert.is_not_nil(span_id) - assert.equal(16, #span_id) -- Should be 16 hex characters - end) - end) - end) - - describe("get_dynamic_sampling_context", function() - it("should return nil when no context", function() - assert.is_nil(propagation.get_dynamic_sampling_context()) - end) - - it("should return DSC when context exists", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local dsc = propagation.get_dynamic_sampling_context() - - assert.is_not_nil(dsc) - assert.equal(context.trace_id, dsc["sentry-trace_id"]) - end) - - it("should return copy of DSC to avoid mutations", function() - local context = propagation.create_context() - propagation.set_current_context(context) - - local dsc1 = propagation.get_dynamic_sampling_context() - local dsc2 = propagation.get_dynamic_sampling_context() - - assert.is_not_equal(dsc1, dsc2) -- Should be different table instances - assert.same(dsc1, dsc2) -- But with same content - end) - end) -end) \ No newline at end of file diff --git a/src/sentry/core/auto_transport.tl b/src/sentry/core/auto_transport.tl deleted file mode 100644 index 011c547..0000000 --- a/src/sentry/core/auto_transport.tl +++ /dev/null @@ -1,64 +0,0 @@ -local transport = require("sentry.core.transport") -local file_io = require("sentry.core.file_io") -local FileTransport = require("sentry.core.file_transport") - -local function detect_platform(): string - if game and game.GetService then - return "roblox" - elseif ngx and ngx.say then - return "nginx" - elseif redis and redis.call then - return "redis" - elseif love and love.graphics then - return "love2d" - elseif sys and sys.get_save_file then - return "defold" - elseif _G.corona then - return "solar2d" - else - return "standard" - end -end - -local function create_auto_transport(config: table): any - local platform = detect_platform() - - if platform == "roblox" then - local roblox_integration = require("sentry.integrations.roblox") - local RobloxTransport = roblox_integration.setup_roblox_integration() - return RobloxTransport:configure(config) - - elseif platform == "nginx" then - local nginx_integration = require("sentry.integrations.nginx") - local NginxTransport = nginx_integration.setup_nginx_integration() - return NginxTransport:configure(config) - - elseif platform == "redis" then - local redis_integration = require("sentry.integrations.redis") - local RedisTransport = redis_integration.setup_redis_integration() - return RedisTransport:configure(config) - - elseif platform == "love2d" then - local love2d_integration = require("sentry.integrations.love2d") - local Love2DTransport = love2d_integration.setup_love2d_integration() - return Love2DTransport:configure(config) - - elseif platform == "defold" then - local defold_file_io = require("sentry.integrations.defold_file_io") - local file_transport = FileTransport:configure({ - dsn = (config as any).dsn, - file_path = "defold-sentry.log", - file_io = defold_file_io.create_defold_file_io() - }) - return file_transport - - else - local HttpTransport = transport.HttpTransport - return HttpTransport:configure(config) - end -end - -return { - detect_platform = detect_platform, - create_auto_transport = create_auto_transport -} \ No newline at end of file diff --git a/src/sentry/core/client.lua b/src/sentry/core/client.lua new file mode 100644 index 0000000..972f2a3 --- /dev/null +++ b/src/sentry/core/client.lua @@ -0,0 +1,36 @@ +local diagnostic_logger = require("core.diagnostic_logger") +local Transport = require("core.transport") +local Client = {} +Client.__index = Client + +function Client:new(options) + if not options then + diagnostic_logger.warn("Cannot create a Sentry client without options.") + return nil + end + if not options.dsn then + diagnostic_logger.warn("Cannot create a Sentry client without DSN.") + return nil + end + local client = setmetatable({ + options = options, + transport = Transport:new(options.dsn, options.transport_options), + }, { __index = self }) + return client +end + +function Client:capture_message(message) + if not self.transport then + diagnostic_logger.error("Transport not initialized") + return nil + end + + local event = { + message = message, + level = "info", + } + + return self.transport:send_event(event) +end + +return Client diff --git a/src/sentry/core/client.tl b/src/sentry/core/client.tl deleted file mode 100644 index 2ae417e..0000000 --- a/src/sentry/core/client.tl +++ /dev/null @@ -1,173 +0,0 @@ -local transport = require("sentry.core.transport") -local Scope = require("sentry.core.scope") -local stacktrace = require("sentry.utils.stacktrace") -local serialize = require("sentry.utils.serialize") -local runtime_utils = require("sentry.utils.runtime") -local os_utils = require("sentry.utils.os") -local types = require("sentry.types") - --- Load platform detectors -require("sentry.platform_loader") - -local SentryOptions = types.SentryOptions - -local record Client - options: SentryOptions -- Configuration from sentry.init() - scope: Scope -- Current scope with user, tags, contexts, etc. - transport: any - enabled: boolean -end - -function Client:new(options: SentryOptions): Client - local client = setmetatable({ - options = options or {} as SentryOptions, - scope = Scope:new(), - enabled = true - }, {__index = Client}) as Client - - -- Configure transport using the registry system - if options.transport then - -- User-provided transport takes precedence - client.transport = options.transport:configure(options) - else - -- Use transport registry to find best available transport - client.transport = transport.create_transport(options) - end - - -- Configure scope max breadcrumbs from options - if options.max_breadcrumbs then - client.scope.max_breadcrumbs = options.max_breadcrumbs - end - - -- Initialize runtime context in scope - local runtime_info = runtime_utils.get_runtime_info() - client.scope:set_context("runtime", { - name = runtime_info.name, - version = runtime_info.version, - description = runtime_info.description - }) - - -- Initialize OS context in scope (if available) - local os_info = os_utils.get_os_info() - if os_info then - local os_context: {string: string} = { - name = os_info.name - } - -- Only add version if it's not nil - if os_info.version then - os_context.version = os_info.version - end - client.scope:set_context("os", os_context) - end - - -- Install Love2D error handler integration if in Love2D environment - if runtime_info.name == "love2d" and (_G as any).love then - local ok, love2d_integration = pcall(require, "sentry.platforms.love2d.integration") - if ok then - local integration = love2d_integration.setup_love2d_integration() - integration:install_error_handler(client) - -- Store integration reference for potential cleanup - client.love2d_integration = integration - end - end - - return client -end - -function Client:is_enabled(): boolean - return self.enabled and self.options.dsn and self.options.dsn ~= "" -end - -function Client:capture_message(message: string, level: string): string - if not self:is_enabled() then - return "" - end - - level = level or "info" - local stack_trace = stacktrace.get_stack_trace(1) - - -- Create basic event - local event = serialize.create_event(level, message, self.options.environment or "production", self.options.release, stack_trace) - - -- Apply scope data to event - event = self.scope:apply_to_event(event) - - if self.options.before_send then - event = self.options.before_send(event) as types.EventData - if not event then - return "" - end - end - - local success, err = (self.transport as any):send(event) - - if self.options.debug then - if success then - print("[Sentry] Event sent: " .. event.event_id) - else - print("[Sentry] Failed to send event: " .. tostring(err)) - end - end - - return success and event.event_id or "" -end - -function Client:capture_exception(exception: table, level: string): string - if not self:is_enabled() then - return "" - end - - level = level or "error" - local stack_trace = stacktrace.get_stack_trace(1) - - local event = serialize.create_event(level, (exception as any).message or "Exception", self.options.environment or "production", self.options.release, stack_trace) - event = self.scope:apply_to_event(event) - (event as any).exception = { - values = {{ - type = (exception as any).type or "Error", - value = (exception as any).message or "Unknown error", - stacktrace = stack_trace - }} - } - - if self.options.before_send then - event = self.options.before_send(event as any) as any - if not event then - return "" - end - end - - local success, err = (self.transport as any):send(event) - - if self.options.debug then - if success then - print("[Sentry] Exception sent: " .. event.event_id) - else - print("[Sentry] Failed to send exception: " .. tostring(err)) - end - end - - return success and event.event_id or "" -end - -function Client:add_breadcrumb(breadcrumb: table) - self.scope:add_breadcrumb(breadcrumb) -end - -function Client:set_user(user: table) - self.scope:set_user(user) -end - -function Client:set_tag(key: string, value: string) - self.scope:set_tag(key, value) -end - -function Client:set_extra(key: string, value: any) - self.scope:set_extra(key, value) -end - -function Client:close() - self.enabled = false -end - -return Client \ No newline at end of file diff --git a/src/sentry/core/context.tl b/src/sentry/core/context.tl deleted file mode 100644 index 74dc1e8..0000000 --- a/src/sentry/core/context.tl +++ /dev/null @@ -1,103 +0,0 @@ -local record Context - user: table - tags: {string: string} - extra: {string: any} - level: string - environment: string - release: string - breadcrumbs: {table} - max_breadcrumbs: number - contexts: {string: any} -- For runtime, os, device contexts -end - -function Context:new(): Context - return setmetatable({ - user = {}, - tags = {}, - extra = {}, - level = "error", - environment = "production", - release = nil, - breadcrumbs = {}, - max_breadcrumbs = 100, - contexts = {} - }, {__index = Context}) as Context -end - -function Context:set_user(user: table) - self.user = user or {} -end - -function Context:set_tag(key: string, value: string) - self.tags[key] = value -end - -function Context:set_extra(key: string, value: any) - self.extra[key] = value -end - -function Context:set_context(key: string, value: any) - self.contexts[key] = value -end - -function Context:set_level(level: string) - local valid_levels: {string: boolean} = {debug = true, info = true, warning = true, error = true, fatal = true} - if valid_levels[level] then - self.level = level - end -end - -function Context:add_breadcrumb(breadcrumb: table) - local crumb: table = { - timestamp = os.time(), - message = breadcrumb.message or "", - category = breadcrumb.category or "default", - level = breadcrumb.level or "info", - data = breadcrumb.data or {} - } - table.insert(self.breadcrumbs, crumb) - - while #self.breadcrumbs > self.max_breadcrumbs do - table.remove(self.breadcrumbs, 1) - end -end - -function Context:clear() - self.user = {} - self.tags = {} - self.extra = {} - self.breadcrumbs = {} - self.contexts = {} -end - -function Context:clone(): Context - local new_context = Context:new() - - for k, v in pairs(self.user) do - new_context.user[k] = v - end - - for k, v in pairs(self.tags) do - new_context.tags[k] = v - end - - for k, v in pairs(self.extra) do - new_context.extra[k] = v - end - - for k, v in pairs(self.contexts) do - new_context.contexts[k] = v - end - - new_context.level = self.level - new_context.environment = self.environment - new_context.release = self.release - - for i, breadcrumb in ipairs(self.breadcrumbs) do - new_context.breadcrumbs[i] = breadcrumb - end - - return new_context -end - -return Context \ No newline at end of file diff --git a/src/sentry/core/diagnostic_logger.lua b/src/sentry/core/diagnostic_logger.lua new file mode 100644 index 0000000..7547abf --- /dev/null +++ b/src/sentry/core/diagnostic_logger.lua @@ -0,0 +1,12 @@ +local diagnostic_logger = {} + +diagnostic_logger.debug = function(message) print("[Sentry] " .. message) end + +diagnostic_logger.warn = function(message) warn("[Sentry] " .. message) end + +diagnostic_logger.error = function(message) + warn("[Sentry] ERROR: " .. message, 2) + -- error("[Sentry] " .. message, 2) +end + +return diagnostic_logger diff --git a/src/sentry/core/dsn.lua b/src/sentry/core/dsn.lua new file mode 100644 index 0000000..e71ee66 --- /dev/null +++ b/src/sentry/core/dsn.lua @@ -0,0 +1,69 @@ +local version = require("core.version") + +local function parse_dsn(dsn_string) + if not dsn_string or dsn_string == "" then return nil, "DSN is required" end + + -- Pattern to match DSN with optional secret key + -- https://public_key:secret_key@host:port/path/project_id + -- https://public_key@host:port/path/project_id + local protocol, credentials, host_path = dsn_string:match("^(https?)://([^@]+)@(.+)$") + + if not protocol or not credentials or not host_path then return nil, "Invalid DSN format" end + + -- Parse credentials (public_key or public_key:secret_key) + local public_key, secret_key = credentials:match("^([^:]+):(.+)$") + if not public_key then + public_key = credentials + secret_key = "" + end + + if not public_key or public_key == "" then return nil, "Invalid DSN format" end + + -- Parse host and path + local host, path = host_path:match("^([^/]+)(.*)$") + if not host or not path or path == "" then return nil, "Invalid DSN format" end + + -- Extract project ID from path (last numeric segment) + local project_id = path:match("/([%d]+)$") + if not project_id then return nil, "Could not extract project ID from DSN" end + + -- Parse port from host + local port = 443 + if protocol == "http" then port = 80 end + + local host_part, port_part = host:match("^([^:]+):?(%d*)$") + if host_part then + host = host_part + if port_part and port_part ~= "" then port = tonumber(port_part) or port end + end + + return { + protocol = protocol, + public_key = public_key, + secret_key = secret_key or "", + host = host, + port = port, + path = path, + project_id = project_id, + }, nil +end + +local function build_envelope_url(dsn) return string.format("%s://%s/api/%s/envelope/", dsn.protocol, dsn.host, dsn.project_id) end + +local function build_auth_header(dsn) + local auth_parts = { + "Sentry sentry_version=7", + "sentry_key=" .. dsn.public_key, + "sentry_client=sentry-lua/" .. version, + } + + if dsn.secret_key and dsn.secret_key ~= "" then table.insert(auth_parts, "sentry_secret=" .. dsn.secret_key) end + + return table.concat(auth_parts, ", ") +end + +return { + parse_dsn = parse_dsn, + build_envelope_url = build_envelope_url, + build_auth_header = build_auth_header, +} diff --git a/src/sentry/core/file_io.tl b/src/sentry/core/file_io.tl deleted file mode 100644 index a581ccb..0000000 --- a/src/sentry/core/file_io.tl +++ /dev/null @@ -1,67 +0,0 @@ -local record FileIO - write_file: function(self: FileIO, path: string, content: string): boolean, string - read_file: function(self: FileIO, path: string): string, string - file_exists: function(self: FileIO, path: string): boolean - ensure_directory: function(self: FileIO, path: string): boolean, string -end - -local record StandardFileIO -end - -function StandardFileIO:write_file(path: string, content: string): boolean, string - local file, err = io.open(path, "w") - if not file then - return false, "Failed to open file: " .. tostring(err) - end - - local success, write_err = file:write(content) - file:close() - - if not success then - return false, "Failed to write file: " .. tostring(write_err) - end - - return true, "File written successfully" -end - -function StandardFileIO:read_file(path: string): string, string - local file, err = io.open(path, "r") - if not file then - return "", "Failed to open file: " .. tostring(err) - end - - local content = file:read("*all") - file:close() - - return content or "", "" -end - -function StandardFileIO:file_exists(path: string): boolean - local file = io.open(path, "r") - if file then - file:close() - return true - end - return false -end - -function StandardFileIO:ensure_directory(path: string): boolean, string - local command = "mkdir -p " .. path - local success = os.execute(command) - - if success then - return true, "Directory created" - else - return false, "Failed to create directory" - end -end - -local function create_standard_file_io(): FileIO - return setmetatable({}, {__index = StandardFileIO}) as FileIO -end - -return { - FileIO = FileIO, - StandardFileIO = StandardFileIO, - create_standard_file_io = create_standard_file_io -} \ No newline at end of file diff --git a/src/sentry/core/file_transport.tl b/src/sentry/core/file_transport.tl deleted file mode 100644 index 9202dd2..0000000 --- a/src/sentry/core/file_transport.tl +++ /dev/null @@ -1,64 +0,0 @@ -local file_io = require("sentry.core.file_io") -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record FileTransport - endpoint: string - timeout: number - headers: {string: string} - file_path: string - file_io: file_io.FileIO - append_mode: boolean -end - -function FileTransport:send(event: table): boolean, string - local serialized = json.encode(event) - local timestamp = os.date("%Y-%m-%d %H:%M:%S") - local content = string.format("[%s] %s\n", timestamp, serialized) - - if self.append_mode and self.file_io:file_exists(self.file_path) then - local existing_content, read_err = self.file_io:read_file(self.file_path) - if read_err ~= "" then - return false, "Failed to read existing file: " .. read_err - end - content = existing_content .. content - end - - local success, err = self.file_io:write_file(self.file_path, content) - - if success then - return true, "Event written to file: " .. self.file_path - else - return false, "Failed to write event: " .. err - end -end - -function FileTransport:configure(config: table): FileTransport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.file_path = (config as any).file_path or "sentry-events.log" - self.append_mode = (config as any).append_mode ~= false - - if (config as any).file_io then - self.file_io = (config as any).file_io - else - self.file_io = file_io.create_standard_file_io() - end - - local dir_path = self.file_path:match("^(.*/)") - if dir_path then - local dir_success, dir_err = self.file_io:ensure_directory(dir_path) - if not dir_success then - print("Warning: Failed to create directory: " .. dir_err) - end - end - - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-file/" .. version - } - - return self -end - -return FileTransport \ No newline at end of file diff --git a/src/sentry/core/scope.lua b/src/sentry/core/scope.lua new file mode 100644 index 0000000..70cb146 --- /dev/null +++ b/src/sentry/core/scope.lua @@ -0,0 +1,42 @@ +local Scope = {} +Scope.__index = Scope + +Scope.user = {} +Scope.tags = {} +Scope.extra = {} +Scope.contexts = {} +Scope.breadcrumbs = {} + +function Scope:new() + print("scope:new") + local scope = setmetatable({ + max_breadcrumbs = 100, + }, { __index = self }) + return scope +end + +function Scope:clone() + local new_scope = Scope:new() + for k, v in pairs(self.user) do + new_scope.user[k] = v + end + return new_scope +end + +function Scope:add_breadcrumb(breadcrumb) + local crumb = { + -- TODO: os.time won't work on Roblox? + -- timestamp = os.time(), + message = breadcrumb.message or "", + category = breadcrumb.category or "default", + level = breadcrumb.level or "info", + data = breadcrumb.data, -- or {} + } + table.insert(self.breadcrumbs, crumb) + + while #self.breadcrumbs > self.max_breadcrumbs do + table.remove(self.breadcrumbs, 1) + end +end + +return Scope diff --git a/src/sentry/core/scope.tl b/src/sentry/core/scope.tl deleted file mode 100644 index ab10f55..0000000 --- a/src/sentry/core/scope.tl +++ /dev/null @@ -1,166 +0,0 @@ --- Scope manages contextual data that gets applied to events --- This includes user, tags, extra data, contexts, breadcrumbs, etc. - -local record Scope - user: table - tags: {string: string} - extra: {string: any} - contexts: {string: any} -- For runtime, os, device contexts - breadcrumbs: {table} - max_breadcrumbs: number - level: string -end - -function Scope:new(): Scope - return setmetatable({ - user = {}, - tags = {}, - extra = {}, - contexts = {}, - breadcrumbs = {}, - max_breadcrumbs = 100, - level = nil -- nil means inherit from client/event - }, {__index = Scope}) as Scope -end - -function Scope:set_user(user: table) - self.user = user or {} -end - -function Scope:set_tag(key: string, value: string) - self.tags[key] = value -end - -function Scope:set_extra(key: string, value: any) - self.extra[key] = value -end - -function Scope:set_context(key: string, value: any) - self.contexts[key] = value -end - -function Scope:set_level(level: string) - local valid_levels: {string: boolean} = {debug = true, info = true, warning = true, error = true, fatal = true} - if valid_levels[level] then - self.level = level - end -end - -function Scope:add_breadcrumb(breadcrumb: table) - local crumb: table = { - timestamp = os.time(), - message = breadcrumb.message or "", - category = breadcrumb.category or "default", - level = breadcrumb.level or "info", - data = breadcrumb.data or {} - } - table.insert(self.breadcrumbs, crumb) - - while #self.breadcrumbs > self.max_breadcrumbs do - table.remove(self.breadcrumbs, 1) - end -end - -function Scope:clear() - self.user = {} - self.tags = {} - self.extra = {} - self.contexts = {} - self.breadcrumbs = {} - self.level = nil -end - -function Scope:clone(): Scope - local new_scope = Scope:new() - - for k, v in pairs(self.user) do - new_scope.user[k] = v - end - - for k, v in pairs(self.tags) do - new_scope.tags[k] = v - end - - for k, v in pairs(self.extra) do - new_scope.extra[k] = v - end - - for k, v in pairs(self.contexts) do - new_scope.contexts[k] = v - end - - new_scope.level = self.level - new_scope.max_breadcrumbs = self.max_breadcrumbs - - for i, breadcrumb in ipairs(self.breadcrumbs) do - new_scope.breadcrumbs[i] = breadcrumb - end - - return new_scope -end - --- Apply scope data to an event (used by Client when creating events) -function Scope:apply_to_event(event: table): table - -- Apply user data - if next(self.user) then - event.user = event.user or {} - for k, v in pairs(self.user) do - event.user[k] = v - end - end - - -- Apply tags - if next(self.tags) then - event.tags = event.tags or {} - for k, v in pairs(self.tags) do - event.tags[k] = v - end - end - - -- Apply extra data - if next(self.extra) then - event.extra = event.extra or {} - for k, v in pairs(self.extra) do - event.extra[k] = v - end - end - - -- Apply contexts - if next(self.contexts) then - event.contexts = event.contexts or {} - for k, v in pairs(self.contexts) do - event.contexts[k] = v - end - end - - -- Apply trace context if active (check for distributed tracing) - local success, tracing = pcall(require, "sentry.tracing.propagation") - if success and tracing and tracing.get_current_context then - local trace_context = tracing.get_current_context() - if trace_context then - event.contexts = event.contexts or {} - event.contexts.trace = { - trace_id = trace_context.trace_id, - span_id = trace_context.span_id, - parent_span_id = trace_context.parent_span_id - } - end - end - - -- Apply breadcrumbs - if #self.breadcrumbs > 0 then - event.breadcrumbs = {} - for i, breadcrumb in ipairs(self.breadcrumbs) do - event.breadcrumbs[i] = breadcrumb - end - end - - -- Apply level if set on scope - if self.level then - event.level = self.level - end - - return event -end - -return Scope \ No newline at end of file diff --git a/src/sentry/core/test_transport.tl b/src/sentry/core/test_transport.tl deleted file mode 100644 index 25d392d..0000000 --- a/src/sentry/core/test_transport.tl +++ /dev/null @@ -1,34 +0,0 @@ -local version = require("sentry.version") - -local record TestTransport - endpoint: string - timeout: number - headers: {string: string} - events: {table} -end - -function TestTransport:send(event: table): boolean, string - table.insert(self.events, event) - return true, "Event captured in test transport" -end - -function TestTransport:configure(config: table): TestTransport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-test/" .. version - } - self.events = {} - return self -end - -function TestTransport:get_events(): {table} - return self.events -end - -function TestTransport:clear_events() - self.events = {} -end - -return TestTransport \ No newline at end of file diff --git a/src/sentry/core/transport.lua b/src/sentry/core/transport.lua new file mode 100644 index 0000000..f86da58 --- /dev/null +++ b/src/sentry/core/transport.lua @@ -0,0 +1,70 @@ +local platform = require("platforms")() +local logger = require("core.diagnostic_logger") + +local Transport = {} +Transport.__index = Transport + +function Transport:new(dsn, options) + options = options or {} + local transport = setmetatable({ + dsn = dsn, + queue = {}, + max_queue_size = options.max_queue_size or 30, + max_concurrent = options.max_concurrent or 5, + timeout_ms = options.timeout_ms or 5000, + processing = false, + active_requests = 0, + }, { __index = self }) + return transport +end + +function Transport:_process_queue() + if self.processing or #self.queue == 0 or self.active_requests >= self.max_concurrent then return end + + self.processing = true + + local event = table.remove(self.queue, 1) + if event then + self.active_requests = self.active_requests + 1 + platform.http_post_async(self.dsn, self:_serialize_event(event), { + ["Content-Type"] = "application/json", + ["User-Agent"] = "sentry-lua/1.0.0", + }, { timeout_ms = self.timeout_ms }, function(ok, status, body) + self.active_requests = self.active_requests - 1 + if not ok then + local msg = "Failed to send event to Sentry: " .. tostring(status) + if body and body ~= "" then msg = msg .. ", response: " .. tostring(body) end + logger.error(msg) + elseif status < 200 or status >= 300 then + local msg = "Sentry responded with error status: " .. tostring(status) + if body and body ~= "" then msg = msg .. ", response: " .. tostring(body) end + logger.error(msg) + else + logger.debug("Event sent to Sentry, status: " .. tostring(status)) + end + self:_process_queue() + end) + end + + self.processing = false + + if #self.queue > 0 and self.active_requests < self.max_concurrent then self:_process_queue() end +end + +function Transport:_serialize_event(event) + local serialized = '{"message":"' .. (event.message or "") .. '","timestamp":"' .. platform.timestamp() .. '"}' + return serialized +end + +function Transport:send_event(event) + if #self.queue >= self.max_queue_size then + logger.warn("Queue full, dropping event") + return false + end + + table.insert(self.queue, event) + self:_process_queue() + return true +end + +return Transport diff --git a/src/sentry/core/transport.tl b/src/sentry/core/transport.tl deleted file mode 100644 index 037960b..0000000 --- a/src/sentry/core/transport.tl +++ /dev/null @@ -1,20 +0,0 @@ --- Platform-agnostic transport interface --- Load all platform-specific transport implementations -require("sentry.platforms.standard.transport") -require("sentry.platforms.standard.file_transport") -require("sentry.platforms.roblox.transport") -require("sentry.platforms.love2d.transport") -require("sentry.platforms.nginx.transport") -require("sentry.platforms.redis.transport") -require("sentry.platforms.defold.transport") -require("sentry.platforms.test.transport") - --- Re-export transport utilities -local transport_utils = require("sentry.utils.transport") - -return { - Transport = transport_utils.Transport, - create_transport = transport_utils.create_transport, - get_available_transports = transport_utils.get_available_transports, - register_transport_factory = transport_utils.register_transport_factory -} \ No newline at end of file diff --git a/src/sentry/version.tl b/src/sentry/core/version.lua similarity index 70% rename from src/sentry/version.tl rename to src/sentry/core/version.lua index ee2128e..a36784c 100644 --- a/src/sentry/version.tl +++ b/src/sentry/core/version.lua @@ -1,6 +1,6 @@ -- Sentry Lua SDK Version -- This file is automatically updated by the bump-version script -local VERSION = "0.0.6" +local VERSION = "0.0.7" -return VERSION \ No newline at end of file +return VERSION diff --git a/src/sentry/init.lua b/src/sentry/init.lua new file mode 100644 index 0000000..cb5ab75 --- /dev/null +++ b/src/sentry/init.lua @@ -0,0 +1,106 @@ +-- If 'debug' is available (not everywhere, e.g: Roblox), use it to make 'require's call relative to local directory +if type(debug) == "table" and type(debug.getinfo) == "function" then + local info = debug.getinfo(1, "S") + if info and type(info.source) == "string" and info.source:sub(1, 1) == "@" then + -- handle / and \ (Windows) + local local_dir = info.source:match("@(.*/)") or info.source:match("@(.*\\)") + if local_dir then package.path = local_dir .. "?.lua;" .. local_dir .. "?/init.lua;" .. package.path end + end +end + +local platform = require("platforms")() +print(platform.name, platform.timestamp()) + +local Client = require("core.client") +local Scope = require("core.scope") +local logger = require("core.diagnostic_logger") + +local sentry = {} + +local function init(options) + logger.debug("init") + sentry._client = Client:new(options) + sentry._scope = Scope:new() +end +local function capture_message(message, level) + if not sentry._client then + logger.debug("Sentry SDK has not been initialized") + return + end + return sentry._client:capture_message(message) +end +local function capture_exception(exception) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("capture_exception") +end +local function add_breadcrumb(exception) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("add_breadcrumb") +end +local function with_scope(callback) + if not sentry._client then + logger.debug("Sentry SDK has not been initialized. Executing callback for program continuity but no error handling will be active") + return + end + + logger.debug("with_scope") + local new_scope = sentry._scope:clone() + + local success, result = pcall(callback, new_scope) + + if not success then logger.error("callback failed to run through with_scope: " .. result) end +end +local function set_tag(key, value) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("set_tag") +end +local function set_extra(key, value) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("set_extra") +end +local function set_user(key, value) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("set_user") +end +local function flush(key, value) + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("flush") +end +local function close() + if not sentry._client then + logger.debug("Sentry not initialized. Call sentry.init() first.") + return + end + logger.debug("close") +end + +sentry.init = init +sentry.capture_message = capture_message +sentry.capture_exception = capture_exception +sentry.add_breadcrumb = add_breadcrumb +sentry.with_scope = with_scope +sentry.set_tag = set_tag +sentry.set_extra = set_extra +sentry.set_user = set_user +sentry.flush = flush +sentry.close = close + +return sentry diff --git a/src/sentry/init.tl b/src/sentry/init.tl deleted file mode 100644 index cf19087..0000000 --- a/src/sentry/init.tl +++ /dev/null @@ -1,150 +0,0 @@ -local Client = require("sentry.core.client") -local Scope = require("sentry.core.scope") - -local record Sentry - _client: Client - init: function(table): Client - capture_message: function(string, string): string - capture_exception: function(table, string): string - add_breadcrumb: function(table) - set_user: function(table) - set_tag: function(string, string) - set_extra: function(string, any) - flush: function() - close: function() - with_scope: function(function(Scope)) - wrap: function(function, function): boolean, any -end - -local sentry: Sentry = {} - -local function init(config: table): Client - if not config or not config.dsn then - error("Sentry DSN is required") - end - - sentry._client = Client:new(config as any) - return sentry._client -end - -local function capture_message(message: string, level: string): string - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - return sentry._client:capture_message(message, level) -end - -local function capture_exception(exception: table, level: string): string - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - return sentry._client:capture_exception(exception, level) -end - -local function add_breadcrumb(breadcrumb: table) - if sentry._client then - sentry._client:add_breadcrumb(breadcrumb) - end -end - -local function set_user(user: table) - if sentry._client then - sentry._client:set_user(user) - end -end - -local function set_tag(key: string, value: string) - if sentry._client then - sentry._client:set_tag(key, value) - end -end - -local function set_extra(key: string, value: any) - if sentry._client then - sentry._client:set_extra(key, value) - end -end - -local function flush() - if sentry._client and sentry._client.transport then - -- Call flush method if available - pcall(function() - (sentry._client.transport as any):flush() - end) - end -end - -local function close() - if sentry._client then - sentry._client:close() - sentry._client = nil - end -end - -local function with_scope(callback: function(scope: Scope)) - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - local original_scope = sentry._client.scope:clone() - - local success, result = pcall(callback, sentry._client.scope) - - sentry._client.scope = original_scope - - if not success then - error(result as any) - end -end - -local function wrap(main_function: function, error_handler: function): boolean, any - if not sentry._client then - error("Sentry not initialized. Call sentry.init() first.") - end - - -- Default error handler that captures to Sentry - local function default_error_handler(err: any): any - -- Add breadcrumb for the unhandled error - add_breadcrumb({ - message = "Unhandled error occurred", - category = "error", - level = "error", - data = { - error_message = tostring(err) - } - }) - - -- Capture the exception with full context - capture_exception({ - type = "UnhandledException", - message = tostring(err) - }, "fatal") - - -- If user provided custom error handler, call it and return its result - if error_handler then - return error_handler(err) - end - - -- Return the error for the caller to handle (don't re-throw to avoid infinite loop) - return tostring(err) - end - - return xpcall(main_function, default_error_handler) -end - --- Set up the sentry module functions -sentry.init = init -sentry.capture_message = capture_message -sentry.capture_exception = capture_exception -sentry.add_breadcrumb = add_breadcrumb -sentry.set_user = set_user -sentry.set_tag = set_tag -sentry.set_extra = set_extra -sentry.flush = flush -sentry.close = close -sentry.with_scope = with_scope -sentry.wrap = wrap - -return sentry \ No newline at end of file diff --git a/src/sentry/logger/init.tl b/src/sentry/logger/init.tl deleted file mode 100644 index dba7fa3..0000000 --- a/src/sentry/logger/init.tl +++ /dev/null @@ -1,436 +0,0 @@ ----@class sentry.logger ---- Sentry logging module for structured log capture and transmission - -local json = require("sentry.utils.json") -local envelope = require("sentry.utils.envelope") -local utils = require("sentry.utils") - --- Type definitions for log records -local record LogLevel - trace: string - debug: string - info: string - warn: string - error: string - fatal: string -end - -local record Attribute - value: string | number | boolean | integer - type: string -end - -local record LogRecord - timestamp: number - trace_id: string - level: string - body: string - attributes: {string: Attribute} - severity_number: integer -end - -local record LogBuffer - logs: {LogRecord} - max_size: integer - flush_timeout: number - last_flush: number -end - -local record LoggerConfig - enable_logs: boolean - before_send_log: function(LogRecord): LogRecord - max_buffer_size: integer - flush_timeout: number - hook_print: boolean -end - --- Log level to severity number mapping -local LOG_LEVELS: LogLevel = { - trace = "trace", - debug = "debug", - info = "info", - warn = "warn", - error = "error", - fatal = "fatal" -} - -local SEVERITY_NUMBERS = { - trace = 1, - debug = 5, - info = 9, - warn = 13, - error = 17, - fatal = 21 -} - --- Module state -local logger = {} -local buffer: LogBuffer -local config: LoggerConfig -local original_print: function -local is_initialized = false - --- Initialize the logger with configuration -function logger.init(user_config: LoggerConfig) - config = { - enable_logs = user_config and user_config.enable_logs or false, - before_send_log = user_config and user_config.before_send_log, - max_buffer_size = user_config and user_config.max_buffer_size or 100, - flush_timeout = user_config and user_config.flush_timeout or 5.0, - hook_print = user_config and user_config.hook_print or false - } - - buffer = { - logs = {}, - max_size = config.max_buffer_size, - flush_timeout = config.flush_timeout, - last_flush = os.time() - } - - is_initialized = true - - -- Hook into print if requested - if config.hook_print then - logger.hook_print() - end -end - --- Get current trace context for log association -local function get_trace_context(): string, string - local success, tracing = pcall(require, "sentry.tracing") - if not success then - return utils.generate_uuid():gsub("-", ""), nil - end - - local trace_info = tracing.get_current_trace_info() - if trace_info and trace_info.trace_id then - return trace_info.trace_id, trace_info.span_id - end - - return utils.generate_uuid():gsub("-", ""), nil -end - --- Get default attributes for a log record -local function get_default_attributes(parent_span_id: string): {string: Attribute} - local attributes: {string: Attribute} = {} - - -- Get SDK info from central location - local version_success, version = pcall(require, "sentry.version") - local sdk_version = version_success and version or "unknown" - - attributes["sentry.sdk.name"] = {value = "sentry.lua", type = "string"} - attributes["sentry.sdk.version"] = {value = sdk_version, type = "string"} - - -- Get Sentry client context if available - local sentry_success, sentry = pcall(require, "sentry") - if sentry_success and sentry._client and sentry._client.config then - local client_config = sentry._client.config - - if client_config.environment then - attributes["sentry.environment"] = {value = client_config.environment, type = "string"} - end - - if client_config.release then - attributes["sentry.release"] = {value = client_config.release, type = "string"} - end - end - - -- Add parent span ID if available - if parent_span_id then - attributes["sentry.trace.parent_span_id"] = {value = parent_span_id, type = "string"} - end - - return attributes -end - --- Create a log record -local function create_log_record(level: string, body: string, template: string, params: {any}, extra_attributes: {string: any}): LogRecord - if not config.enable_logs then - return nil - end - - local trace_id, parent_span_id = get_trace_context() - local attributes = get_default_attributes(parent_span_id) - - -- Add template and parameters if provided - if template then - attributes["sentry.message.template"] = {value = template, type = "string"} - - if params then - for i, param in ipairs(params) do - local param_key = "sentry.message.parameter." .. tostring(i - 1) - local param_type = type(param) - - if param_type == "number" then - if math.floor(param) == param then - attributes[param_key] = {value = param, type = "integer"} - else - attributes[param_key] = {value = param, type = "double"} - end - elseif param_type == "boolean" then - attributes[param_key] = {value = param, type = "boolean"} - else - attributes[param_key] = {value = tostring(param), type = "string"} - end - end - end - end - - -- Add extra attributes - if extra_attributes then - for key, value in pairs(extra_attributes) do - local value_type = type(value) - if value_type == "number" then - if math.floor(value) == value then - attributes[key] = {value = value, type = "integer"} - else - attributes[key] = {value = value, type = "double"} - end - elseif value_type == "boolean" then - attributes[key] = {value = value, type = "boolean"} - else - attributes[key] = {value = tostring(value), type = "string"} - end - end - end - - local record: LogRecord = { - timestamp = os.time() + (os.clock() % 1), - trace_id = trace_id, - level = level, - body = body, - attributes = attributes, - severity_number = SEVERITY_NUMBERS[level] or 9 - } - - return record -end - --- Add a log record to the buffer -local function add_to_buffer(record: LogRecord) - if not record or not buffer then - return - end - - -- Apply before_send_log hook if configured - if config.before_send_log then - record = config.before_send_log(record) - if not record then - return -- Log was filtered out - end - end - - table.insert(buffer.logs, record) - - -- Check if we need to flush - local should_flush = false - - if #buffer.logs >= buffer.max_size then - should_flush = true - elseif buffer.flush_timeout > 0 then - local current_time = os.time() - if (current_time - buffer.last_flush) >= buffer.flush_timeout then - should_flush = true - end - end - - if should_flush then - logger.flush() - end -end - --- Flush the log buffer -function logger.flush() - if not buffer or #buffer.logs == 0 then - return - end - - -- Get Sentry client - local sentry_success, sentry = pcall(require, "sentry") - if not sentry_success or not sentry._client or not sentry._client.transport then - -- Clear buffer even if we can't send - buffer.logs = {} - buffer.last_flush = os.time() - return - end - - -- Get version from central location - local version_success, version = pcall(require, "sentry.version") - local sdk_version = version_success and version or "unknown" - - -- Build log envelope using the proper log format - local envelope_body = envelope.build_log_envelope(buffer.logs) - - -- Send via envelope transport only (no fallback to /store endpoint) - if sentry._client.transport.send_envelope then - local success, err = sentry._client.transport:send_envelope(envelope_body) - if success then - print("[Sentry] Sent " .. #buffer.logs .. " log records via envelope") - else - print("[Sentry] Failed to send log envelope: " .. tostring(err)) - end - else - print("[Sentry] No envelope transport available for logs") - end - - -- Clear buffer - buffer.logs = {} - buffer.last_flush = os.time() -end - --- Core logging function -local function log(level: string, message: string, template: string, params: {any}, attributes: {string: any}) - if not is_initialized or not config.enable_logs then - return - end - - local record = create_log_record(level, message, template, params, attributes) - if record then - add_to_buffer(record) - end -end - --- String formatting helper (similar to printf) -local function format_message(template: string, ...: any): string, {any} - local args = {...} - local formatted = template - - -- Simple %s replacement - local i = 1 - formatted = formatted:gsub("%%s", function() - local arg = args[i] - i = i + 1 - return tostring(arg or "") - end) - - return formatted, args -end - --- Public logging API functions -function logger.trace(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("trace", formatted, message, args, attributes) - else - log("trace", message, nil, nil, attributes or params) - end -end - -function logger.debug(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("debug", formatted, message, args, attributes) - else - log("debug", message, nil, nil, attributes or params) - end -end - -function logger.info(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("info", formatted, message, args, attributes) - else - log("info", message, nil, nil, attributes or params) - end -end - -function logger.warn(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("warn", formatted, message, args, attributes) - else - log("warn", message, nil, nil, attributes or params) - end -end - -function logger.error(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("error", formatted, message, args, attributes) - else - log("error", message, nil, nil, attributes or params) - end -end - -function logger.fatal(message: string, params: {any}, attributes: {string: any}) - if type(message) == "string" and message:find("%%s") and params then - local formatted, args = format_message(message, table.unpack(params)) - log("fatal", formatted, message, args, attributes) - else - log("fatal", message, nil, nil, attributes or params) - end -end - --- Print hooking functionality -function logger.hook_print() - if original_print then - return -- Already hooked - end - - original_print = print - - -- Flag to prevent recursion - local in_sentry_print = false - - _G.print = function(...: any) - -- Call original print first - original_print(...) - - -- Avoid recursion from Sentry's own print statements - if in_sentry_print then - return - end - - if not is_initialized or not config.enable_logs then - return - end - - in_sentry_print = true - - -- Convert arguments to string - local args = {...} - local parts = {} - for i, arg in ipairs(args) do - parts[i] = tostring(arg) - end - local message = table.concat(parts, "\t") - - -- Create log record with sentry.origin - local record = create_log_record("info", message, nil, nil, { - ["sentry.origin"] = "auto.logging.print" - }) - - if record then - add_to_buffer(record) - end - - in_sentry_print = false - end -end - -function logger.unhook_print() - if original_print then - _G.print = original_print - original_print = nil - end -end - --- Get current configuration -function logger.get_config(): LoggerConfig - return config -end - --- Get buffer status for debugging -function logger.get_buffer_status(): {string: any} - if not buffer then - return {logs = 0, max_size = 0, last_flush = 0} - end - - return { - logs = #buffer.logs, - max_size = buffer.max_size, - last_flush = buffer.last_flush - } -end - -return logger \ No newline at end of file diff --git a/src/sentry/performance/init.tl b/src/sentry/performance/init.tl deleted file mode 100644 index 1b596ca..0000000 --- a/src/sentry/performance/init.tl +++ /dev/null @@ -1,352 +0,0 @@ ----@class sentry.performance ---- Object-oriented performance monitoring without global state - -local headers = require("sentry.tracing.headers") - -local record Transaction - span_id: string - parent_span_id: string - trace_id: string - op: string - description: string - status: string - start_timestamp: number - timestamp: number - - -- Transaction-specific fields - event_id: string - type: string - transaction: string - spans: {Span} - contexts: {string: any} - tags: {string: string} - extra: {string: any} - finished: boolean - active_spans: {Span} -- Stack for child spans - - -- Methods - finish: function(self: Transaction, status: string) - start_span: function(self: Transaction, op: string, description: string, options: table): Span - add_tag: function(self: Transaction, key: string, value: string) - add_data: function(self: Transaction, key: string, value: any) -end - -local record Span - span_id: string - parent_span_id: string - trace_id: string - op: string - description: string - status: string - tags: {string: string} - data: {string: any} - start_timestamp: number - timestamp: number - origin: string - finished: boolean - transaction: Transaction -- Reference to parent transaction - - -- Methods - finish: function(self: Span, status: string) - start_span: function(self: Span, op: string, description: string, options: table): Span - add_tag: function(self: Span, key: string, value: string) - add_data: function(self: Span, key: string, value: any) -end - -local performance = {} - ----Generate a high-precision timestamp ----@return number timestamp Unix timestamp with microsecond precision -local function get_timestamp(): number - return os.time() + (os.clock() % 1) -end - ----Get SDK version from central location ----@return string version SDK version or "unknown" -local function get_sdk_version(): string - local success, version = pcall(require, "sentry.version") - return success and version or "unknown" -end - ----Span methods (defined first since transaction methods reference it) -local span_mt = {} -span_mt.__index = span_mt - ----Transaction methods -local transaction_mt = {} -transaction_mt.__index = transaction_mt - -function transaction_mt:finish(status: string) - if self.finished then - return - end - - self.timestamp = get_timestamp() - self.status = status or "ok" - self.finished = true - - -- Send transaction to Sentry using the main client - local sentry = require("sentry") - - local transaction_data = { - event_id = self.event_id, - type = "transaction", - transaction = self.transaction, - start_timestamp = self.start_timestamp, - timestamp = self.timestamp, - spans = self.spans, - contexts = self.contexts, - tags = self.tags, - extra = self.extra, - platform = "lua", - sdk = { - name = "sentry.lua", - version = get_sdk_version() - } - } - - -- Add trace context - transaction_data.contexts = transaction_data.contexts or {} - transaction_data.contexts.trace = { - trace_id = self.trace_id, - span_id = self.span_id, - parent_span_id = self.parent_span_id, - op = self.op, - status = self.status - } - - -- Use the main sentry client to send the transaction - if sentry._client then - local envelope = require("sentry.utils.envelope") - if sentry._client.transport and sentry._client.transport.send_envelope then - local envelope_data = envelope.build_transaction_envelope(transaction_data, self.event_id) - local transport_success, err = sentry._client.transport:send_envelope(envelope_data) - - if transport_success then - print("[Sentry] Transaction sent: " .. self.event_id) - else - print("[Sentry] Failed to send transaction: " .. tostring(err)) - end - end - end -end - -function transaction_mt:start_span(op: string, description: string, options: table): Span - options = options or {} - - local parent_span_id = #self.active_spans > 0 and self.active_spans[#self.active_spans].span_id or self.span_id - - local span_data: Span = { - span_id = headers.generate_span_id(), - parent_span_id = parent_span_id, - trace_id = self.trace_id, - op = op, - description = description, - status = "ok", - tags = options.tags or {}, - data = options.data or {}, - start_timestamp = get_timestamp(), - timestamp = 0, -- Will be set when finished - origin = options.origin or "manual", - finished = false, - transaction = self - } - - -- Set up span methods - setmetatable(span_data, span_mt) - - table.insert(self.active_spans, span_data) - - -- Update propagation context to reflect the current active span - local success, propagation = pcall(require, "sentry.tracing.propagation") - if success then - local propagation_context = { - trace_id = span_data.trace_id, - span_id = span_data.span_id, - parent_span_id = span_data.parent_span_id, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - propagation.set_current_context(propagation_context) - end - - return span_data -end - -function transaction_mt:add_tag(key: string, value: string) - self.tags = self.tags or {} - self.tags[key] = value -end - -function transaction_mt:add_data(key: string, value: any) - self.extra = self.extra or {} - self.extra[key] = value -end - - -function span_mt:finish(status: string) - if self.finished then - return - end - - self.timestamp = get_timestamp() - self.status = status or "ok" - self.finished = true - - -- Remove from active spans stack - local tx = self.transaction - for i = #tx.active_spans, 1, -1 do - if tx.active_spans[i].span_id == self.span_id then - table.remove(tx.active_spans, i) - break - end - end - - -- Add to completed spans - table.insert(tx.spans, { - span_id = self.span_id, - parent_span_id = self.parent_span_id, - trace_id = self.trace_id, - op = self.op, - description = self.description, - status = self.status, - tags = self.tags, - data = self.data, - start_timestamp = self.start_timestamp, - timestamp = self.timestamp, - origin = self.origin - }) - - -- Update propagation context (revert to parent context) - local success, propagation = pcall(require, "sentry.tracing.propagation") - if success then - local parent_context - if #tx.active_spans > 0 then - -- Still have active spans, use the most recent one - local active_span = tx.active_spans[#tx.active_spans] - parent_context = { - trace_id = active_span.trace_id, - span_id = active_span.span_id, - parent_span_id = active_span.parent_span_id, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - else - -- No active spans, revert to transaction context - parent_context = { - trace_id = tx.trace_id, - span_id = tx.span_id, - parent_span_id = tx.parent_span_id, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - end - propagation.set_current_context(parent_context) - end -end - -function span_mt:start_span(op: string, description: string, options: table): Span - -- Delegate to transaction - return self.transaction:start_span(op, description, options) -end - -function span_mt:add_tag(key: string, value: string) - self.tags = self.tags or {} - self.tags[key] = value -end - -function span_mt:add_data(key: string, value: any) - self.data = self.data or {} - self.data[key] = value -end - ----Start a new transaction ----@param name string Transaction name ----@param op string Operation type (e.g., "http.server", "navigation") ----@param options table? Additional options ----@return Transaction transaction The started transaction -function performance.start_transaction(name: string, op: string, options: table): Transaction - options = options or {} - - -- Check for existing propagation context first - local trace_id = options.trace_id - local parent_span_id = options.parent_span_id - local span_id = options.span_id - - if not trace_id or not span_id then - local success, propagation = pcall(require, "sentry.tracing.propagation") - if success then - local context = propagation.get_current_context() - if context then - -- Continue existing trace - trace_id = trace_id or context.trace_id - parent_span_id = parent_span_id or context.span_id -- Current context becomes parent - span_id = span_id or headers.generate_span_id() -- Generate new span for transaction - else - -- No context exists, create new trace context automatically - context = propagation.start_new_trace() - trace_id = trace_id or context.trace_id - span_id = span_id or headers.generate_span_id() - end - end - end - - -- Fallback to generating new trace if no context exists - trace_id = trace_id or headers.generate_trace_id() - span_id = span_id or headers.generate_span_id() - local start_time = get_timestamp() - - local transaction: Transaction = { - event_id = require("sentry.utils").generate_uuid(), - type = "transaction", - transaction = name, - start_timestamp = start_time, - timestamp = start_time, -- Will be updated when finished - spans = {}, - contexts = { - trace = { - trace_id = trace_id, - span_id = span_id, - parent_span_id = parent_span_id, - op = op, - status = "unknown" - } - }, - tags = options.tags or {}, - extra = options.extra or {}, - - -- Span fields - span_id = span_id, - parent_span_id = parent_span_id, - trace_id = trace_id, - op = op, - description = name, - status = "ok", - finished = false, - active_spans = {} - } - - -- Set up transaction methods - setmetatable(transaction, transaction_mt) - - -- Update propagation context to the transaction - local success, propagation = pcall(require, "sentry.tracing.propagation") - if success then - local propagation_context = { - trace_id = transaction.trace_id, - span_id = transaction.span_id, - parent_span_id = transaction.parent_span_id, - sampled = true, - baggage = {}, - dynamic_sampling_context = {} - } - propagation.set_current_context(propagation_context) - end - - return transaction -end - -return performance \ No newline at end of file diff --git a/src/sentry/platform_loader.tl b/src/sentry/platform_loader.tl deleted file mode 100644 index 5e4fc2d..0000000 --- a/src/sentry/platform_loader.tl +++ /dev/null @@ -1,22 +0,0 @@ --- Platform loader: automatically loads available platform detectors --- This file tries to load platform-specific modules that register themselves - -local function load_platforms() - local platform_modules = { - "sentry.platforms.standard.os_detection", - "sentry.platforms.roblox.os_detection", - "sentry.platforms.love2d.os_detection", - "sentry.platforms.nginx.os_detection" - } - - for _, module_name in ipairs(platform_modules) do - pcall(require, module_name) - end -end - --- Load platforms immediately -load_platforms() - -return { - load_platforms = load_platforms -} \ No newline at end of file diff --git a/src/sentry/platforms/defold.lua b/src/sentry/platforms/defold.lua new file mode 100644 index 0000000..f170cee --- /dev/null +++ b/src/sentry/platforms/defold.lua @@ -0,0 +1,58 @@ +-- NOTE: This module depends on the Defold environment. It should only be loaded after verifying it is indeed on Defold + +local function tz_suffix(t) + local z = os.date("%z", t or os.time()) or "+0000" + return string.format("%s%s:%s", z:sub(1, 1), z:sub(2, 3), z:sub(4, 5)) +end + +local M = { name = "defold" } + +function M.timestamp() + local sec = os.time() + local ms = 0 -- Defold core Lua lacks a standard ms wall clock + local d = os.date("*t", sec) + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, ms, tz_suffix(sec)) +end + +function M.http_post(url, body, headers, opts) + headers = headers or {} + local result, done = { ok = false }, false + + ---@diagnostic disable-next-line: undefined-global + http.request(url, "POST", function(self, id, response) + result.ok = (response.status >= 200 and response.status < 400) + result.status = response.status + result.body = response.response + result.headers = response.headers + done = true + end, headers, body or "") + + -- block until done (must be inside coroutine or engine update loop) + while not done do + coroutine.yield() + end + if not result.ok then return nil, tostring(result.status or "request failed") end + return true, result.status, result.body, result.headers +end + +function M.http_post_async(url, body, headers, _, callback) + headers = headers or {} + ---@diagnostic disable-next-line: undefined-global + http.request(url, "POST", function(self, id, response) + if callback then + local ok = (response.status >= 200 and response.status < 400) + callback(ok, response.status, response.response) + end + end, headers, body or "") +end + +function M.sleep(ms) + local t0 = os.clock() + local target = (ms or 0) / 1000 + while os.clock() - t0 < target do + coroutine.yield() + end + return true +end + +return M diff --git a/src/sentry/platforms/defold/file_io.tl b/src/sentry/platforms/defold/file_io.tl deleted file mode 100644 index a917596..0000000 --- a/src/sentry/platforms/defold/file_io.tl +++ /dev/null @@ -1,77 +0,0 @@ -local file_io = require("sentry.core.file_io") - -local record DefoldFileIO -end - -function DefoldFileIO:write_file(path: string, content: string): boolean, string - if not sys then - return false, "Defold sys module not available" - end - - local success, err = pcall(function() - local save_path = sys.get_save_file("sentry", path) - local file = io.open(save_path, "w") - if file then - file:write(content) - file:close() - else - error("Failed to open file for writing") - end - end) - - if success then - return true, "Event written to Defold save file" - else - return false, "Defold file error: " .. tostring(err) - end -end - -function DefoldFileIO:read_file(path: string): string, string - if not sys then - return "", "Defold sys module not available" - end - - local success, result = pcall(function() - local save_path = sys.get_save_file("sentry", path) - local file = io.open(save_path, "r") - if file then - local content = file:read("*all") - file:close() - return content - end - return "" - end) - - if success then - return result or "", "" - else - return "", "Failed to read Defold file: " .. tostring(result) - end -end - -function DefoldFileIO:file_exists(path: string): boolean - if not sys then - return false - end - - local save_path = sys.get_save_file("sentry", path) - local file = io.open(save_path, "r") - if file then - file:close() - return true - end - return false -end - -function DefoldFileIO:ensure_directory(path: string): boolean, string - return true, "Defold handles save directories automatically" -end - -local function create_defold_file_io(): file_io.FileIO - return setmetatable({}, {__index = DefoldFileIO}) as file_io.FileIO -end - -return { - DefoldFileIO = DefoldFileIO, - create_defold_file_io = create_defold_file_io -} \ No newline at end of file diff --git a/src/sentry/platforms/defold/transport.tl b/src/sentry/platforms/defold/transport.tl deleted file mode 100644 index ecb6633..0000000 --- a/src/sentry/platforms/defold/transport.tl +++ /dev/null @@ -1,71 +0,0 @@ --- Defold transport (similar to file transport but optimized for Defold) -local transport_utils = require("sentry.utils.transport") -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record DefoldTransport - endpoint: string - timeout: number - headers: {string: string} - event_queue: {table} -end - -function DefoldTransport:send(event: table): boolean, string - -- In Defold, we typically queue events and process them later - table.insert(self.event_queue, event) - - return true, "Event queued for Defold processing" -end - -function DefoldTransport:configure(config: table): transport_utils.Transport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.event_queue = {} - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-defold/" .. version - } - return self as transport_utils.Transport -end - --- Flush queued events (call this from your Defold script) -function DefoldTransport:flush() - if #self.event_queue == 0 then - return - end - - for _, event in ipairs(self.event_queue) do - local body = json.encode(event) - -- In Defold, you would implement HTTP requests or file writing here - print("[Sentry] Would send event: " .. ((event as any).event_id or "unknown")) - end - - self.event_queue = {} -end - --- Create factory function -local function create_defold_transport(config: table): transport_utils.Transport - local transport = DefoldTransport - return transport:configure(config) -end - --- Check if Defold transport is available (basic check) -local function is_defold_available(): boolean - -- We could check for Defold-specific globals here if available - -- For now, we'll assume it's always available as a fallback - return false -- Set to false so it doesn't interfere with other transports -end - --- Register this transport factory with low priority -transport_utils.register_transport_factory({ - name = "defold", - priority = 50, -- Lower priority - specialized use - create = create_defold_transport, - is_available = is_defold_available -}) - -return { - DefoldTransport = DefoldTransport, - create_defold_transport = create_defold_transport, - is_defold_available = is_defold_available -} \ No newline at end of file diff --git a/src/sentry/platforms/init.lua b/src/sentry/platforms/init.lua new file mode 100644 index 0000000..10741f4 --- /dev/null +++ b/src/sentry/platforms/init.lua @@ -0,0 +1,50 @@ +local M = nil -- will cache the chosen adapter + +local function detect() + -- OpenResty: ngx present with config + ---@diagnostic disable-next-line: undefined-global + if rawget(_G, "ngx") and ngx and ngx.config then return require("platforms.openresty") end + -- Roblox: DateTime + HttpService/Game presence is a good signal + if rawget(_G, "DateTime") and rawget(_G, "game") then return require("platforms.roblox") end + + -- Defold: global 'http' (http.request), 'sys' module, and go module exist + if rawget(_G, "http") and rawget(_G, "sys") and rawget(_G, "go") then return require("platforms.defold") end + + -- LÖVE: global 'love' with version + ---@diagnostic disable-next-line: undefined-global + if rawget(_G, "love") and type(love) == "table" and love._version then return require("platforms.love2d") end + + -- Fallback to standard Lua + return require("platforms.lua") +end + +local function debug(err) + print("Failed loading platform: " .. err) + ---@diagnostic disable-next-line: undefined-global + print(_VERSION, jit and jit.version or "no-jit") + print("cwd:", (io.popen and io.popen("pwd"):read("*l")) or "") + print("package.path =", package.path) + print("package.cpath =", package.cpath) + + local ok1, ssl = pcall(require, "ssl") + print("require ssl =", ok1, ok1 and (ssl._VERSION or "") or ssl) + + local ok2, https = pcall(require, "ssl.https") + print("require ssl.https =", ok2, ok2 and "ok" or https) +end + +local function get() + if not M then + local ok, val = xpcall(detect, debug) + if not ok then error("failed to load Sentry") end + M = val + end + assert(M.http_post, "http_post missing on " .. M.name) + assert(M.timestamp, "timestamp missing on " .. M.name) + return M +end + +return setmetatable({}, { + __index = function(_, k) return get()[k] end, -- proxy functions/fields + __call = function() return get() end, -- allow require(... )() to fetch table +}) diff --git a/src/sentry/platforms/love2d.lua b/src/sentry/platforms/love2d.lua new file mode 100644 index 0000000..1115464 --- /dev/null +++ b/src/sentry/platforms/love2d.lua @@ -0,0 +1,107 @@ +---@diagnostic disable: undefined-global +-- NOTE: This module depends on the LǑVE environment. It should only be loaded after verifying it is indeed on love2d + +local M = { name = "love2d" } + +-- Required imports +local function safe_require(n) + local ok, m = pcall(require, n) + return ok and m or nil +end +local https = safe_require("ssl.https") +if not https then error("https not available (install LuaSec: ssl.https)") end +local ltn12 = safe_require("ltn12") +if not ltn12 then error("ltn12 not available (install ltn12)") end + +local function tz_suffix(t) + local z = os.date("%z", t or os.time()) or "+0000" + assert(type(z) == "string") + return string.format("%s%s:%s", z:sub(1, 1), z:sub(2, 3), z:sub(4, 5)) +end + +-- Try to grab LuaSocket once +local socket = safe_require("socket") + +-- Capture baseline for love.timer blending if needed +local base_wall = os.time() +local base_mono = (love and love.timer and love.timer.getTime and love.timer.getTime()) or 0 + +-- Choose implementation once +local timestamp_impl +if socket and type(socket.gettime) == "function" then + -- Best: LuaSocket with ms precision + timestamp_impl = function() + local now = socket.gettime() + local sec = math.floor(now) + local ms = math.floor((now - sec) * 1000 + 0.5) + if ms == 1000 then + ms, sec = 0, sec + 1 + end + local d = os.date("*t", sec) + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, ms, tz_suffix(sec)) + end +elseif love and love.timer and love.timer.getTime then + -- Fallback: blend os.time() with love.timer high-res clock + timestamp_impl = function() + local elapsed = love.timer.getTime() - base_mono + local now = base_wall + elapsed + local sec = math.floor(now) + local ms = math.floor((now - sec) * 1000 + 0.5) + if ms == 1000 then + ms, sec = 0, sec + 1 + end + local d = os.date("*t", sec) + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, ms, tz_suffix(sec)) + end +else + -- Last resort: os.time() only + timestamp_impl = function() + local sec, ms = os.time(), 0 + local d = os.date("*t", sec) + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, ms, tz_suffix(sec)) + end +end + +-- Public API +function M.timestamp() return timestamp_impl() end + +local function set_timeout(lib, ms) + if lib and ms then lib.TIMEOUT = math.max(0.001, ms / 1000) end +end + +local function request_with_optional_headers(lib, url, headers, body, timeout_ms) + set_timeout(lib, timeout_ms) + headers = headers or {} + body = body or "" + + if ltn12 then + local chunks = {} + local ok, code, resp_headers = lib.request({ + url = url, + method = "POST", + headers = headers, + source = ltn12.source.string(body), + sink = ltn12.sink.table(chunks), + }) + if not ok then return nil, tostring(code or "request failed") end + return true, tonumber(code), table.concat(chunks), resp_headers + end + + local resp_body, code, resp_headers = lib.request(url, body) + if not resp_body then return nil, tostring(code or "request failed") end + return true, tonumber(code), resp_body, resp_headers +end + +function M.http_post(url, body, headers, opts) + opts = opts or {} + return request_with_optional_headers(https, url, headers, body, opts.timeout_ms) +end + +function M.http_post_async(url, body, headers, opts, callback) + -- luacheck: no unused secondaries + ---@diagnostic disable-next-line: unused-local + local ok, status, resp_body, resp_headers = M.http_post(url, body, headers, opts) + if callback then callback(ok, status, resp_body) end +end + +return M diff --git a/src/sentry/platforms/love2d/context.tl b/src/sentry/platforms/love2d/context.tl deleted file mode 100644 index e96fa16..0000000 --- a/src/sentry/platforms/love2d/context.tl +++ /dev/null @@ -1,27 +0,0 @@ --- LÖVE 2D specific context information -local function get_love2d_context(): table - local context = {} - - if _G.love then - local love = _G.love as any - context.love_version = love.getVersion and table.concat({love.getVersion()}, ".") or "unknown" - - if love.graphics then - local w, h = love.graphics.getDimensions() - context.screen = { - width = w, - height = h - } - end - - if love.system then - context.os = love.system.getOS() - end - end - - return context -end - -return { - get_love2d_context = get_love2d_context -} \ No newline at end of file diff --git a/src/sentry/platforms/love2d/integration.tl b/src/sentry/platforms/love2d/integration.tl deleted file mode 100644 index c112f49..0000000 --- a/src/sentry/platforms/love2d/integration.tl +++ /dev/null @@ -1,174 +0,0 @@ --- Love2D Integration with error handler hooking -local transport_utils = require("sentry.utils.transport") - -local record Love2DIntegration - transport: transport_utils.Transport - original_errorhandler: function | nil - sentry_client: any - - configure: function(Love2DIntegration, table): transport_utils.Transport - install_error_handler: function(Love2DIntegration, any) - uninstall_error_handler: function(Love2DIntegration) -end - --- Hook into love.errorhandler to capture fatal errors -local function hook_error_handler(client: any): function, function - local original_errorhandler = (_G as any).love and (_G as any).love.errorhandler - - local function sentry_errorhandler(msg: string): any - -- Capture the error in Sentry as a fatal exception - if client then - -- Create exception mechanism with handled: false (fatal) - local exception: {string: any} = { - type = "RuntimeError", - value = tostring(msg), - mechanism = { - type = "love.errorhandler", - handled = false, - synthetic = false - } - } - - -- Add stacktrace - local stacktrace = debug.traceback(msg, 2) - if stacktrace then - exception.stacktrace = { - frames = {} -- TODO: Parse stacktrace frames if needed - } - end - - -- Create Sentry event - local event: {string: any} = { - level = "fatal", - exception = { - values = {exception} - }, - extra = { - error_message = tostring(msg), - love_errorhandler = true - } - } - - -- Create a proper Sentry event with mechanism.handled: false - local stacktrace = require("sentry.utils.stacktrace") - local serialize = require("sentry.utils.serialize") - local stack_trace = stacktrace.get_stack_trace(2) - - -- Create event with proper exception mechanism - local event = serialize.create_event("fatal", tostring(msg), - client.options.environment or "production", - client.options.release, stack_trace) - - -- Add exception with mechanism.handled: false - event.exception = { - values = {{ - type = "RuntimeError", - value = tostring(msg), - mechanism = { - type = "love.errorhandler", - handled = false, - synthetic = false - }, - stacktrace = stack_trace - }} - } - - -- Apply scope data to event - event = client.scope:apply_to_event(event) - - -- Apply before_send hook if configured - if client.options.before_send then - event = client.options.before_send(event) - if not event then - return -- Event was filtered out - end - end - - -- Send event directly via transport - if client.transport then - local success, err = client.transport:send(event) - if client.options.debug then - if success then - print("[Sentry] Fatal error sent: " .. event.event_id) - else - print("[Sentry] Failed to send fatal error: " .. tostring(err)) - end - end - - -- Force immediate flush to ensure error is sent before app crashes - if client.transport.flush then - client.transport:flush() - end - end - end - - -- Call original error handler if it exists, or create default behavior - if original_errorhandler then - local ok, result = xpcall(original_errorhandler, debug.traceback, msg) - if ok then - return result - else - -- Original error handler failed, create minimal error screen - print("Error in original love.errorhandler:", result) - end - end - - -- Default error handling behavior (Love2D will crash after this) - print("Fatal error:", msg) - print(debug.traceback()) - - -- Re-throw the error to ensure Love2D crashes as expected - error(msg) - end - - return sentry_errorhandler, original_errorhandler -end - --- Setup Love2D integration -local function setup_love2d_integration(): Love2DIntegration - local love2d_transport = require("sentry.platforms.love2d.transport") - - if not love2d_transport.is_love2d_available() then - error("Love2D integration can only be used in Love2D environment") - end - - local integration: Love2DIntegration = {} as Love2DIntegration - integration.transport = nil - integration.original_errorhandler = nil - integration.sentry_client = nil - - function integration:configure(config: table): transport_utils.Transport - self.transport = love2d_transport.create_love2d_transport(config) - return self.transport - end - - function integration:install_error_handler(client: any) - if not (_G as any).love then - return - end - - self.sentry_client = client - local sentry_handler, original = hook_error_handler(client) - self.original_errorhandler = original - - -- Install our error handler - (_G as any).love.errorhandler = sentry_handler - - print("✅ Love2D error handler integration installed") - end - - function integration:uninstall_error_handler() - if (_G as any).love and self.original_errorhandler then - (_G as any).love.errorhandler = self.original_errorhandler - self.original_errorhandler = nil - print("✅ Love2D error handler integration uninstalled") - end - end - - return integration -end - -return { - setup_love2d_integration = setup_love2d_integration, - hook_error_handler = hook_error_handler -} \ No newline at end of file diff --git a/src/sentry/platforms/love2d/os_detection.tl b/src/sentry/platforms/love2d/os_detection.tl deleted file mode 100644 index 55812a9..0000000 --- a/src/sentry/platforms/love2d/os_detection.tl +++ /dev/null @@ -1,24 +0,0 @@ -local os_utils = require("sentry.utils.os") -local OSInfo = os_utils.OSInfo - -local function detect_os(): OSInfo - if _G.love and (_G.love as any).system then - local os_name = (_G.love as any).system.getOS() - if os_name then - return { - name = os_name, - version = nil - } - end - end - return nil -end - --- Register this detector -os_utils.register_detector({ - detect = detect_os -}) - -return { - detect_os = detect_os -} \ No newline at end of file diff --git a/src/sentry/platforms/love2d/transport.tl b/src/sentry/platforms/love2d/transport.tl deleted file mode 100644 index 923cbe9..0000000 --- a/src/sentry/platforms/love2d/transport.tl +++ /dev/null @@ -1,159 +0,0 @@ --- LÖVE 2D transport with lua-https support -local transport_utils = require("sentry.utils.transport") -local json = require("sentry.utils.json") -local version = require("sentry.version") -local dsn_utils = require("sentry.utils.dsn") - -local record Love2DTransport - endpoint: string - envelope_endpoint: string - timeout: number - headers: {string: string} - envelope_headers: {string: string} - event_queue: {table} - envelope_queue: {string} - dsn_info: any - send: function(self: Love2DTransport, event: table): boolean, string - send_envelope: function(self: Love2DTransport, envelope_body: string): boolean, string - configure: function(self: Love2DTransport, config: table): transport_utils.Transport - flush: function(self: Love2DTransport) - close: function(self: Love2DTransport) -end - -function Love2DTransport:send(event: table): boolean, string - -- Check if we're in LÖVE 2D environment - local love_global = rawget(_G, "love") - if not love_global then - return false, "Not in LÖVE 2D environment" - end - - -- Queue the event for processing - table.insert(self.event_queue, event) - - -- Process immediately in Love2D main thread - self:flush() - - return true, "Event queued for sending in LÖVE 2D" -end - -function Love2DTransport:send_envelope(envelope_body: string): boolean, string - -- Check if we're in LÖVE 2D environment - local love_global = rawget(_G, "love") - if not love_global then - return false, "Not in LÖVE 2D environment" - end - - -- Queue the envelope for processing - table.insert(self.envelope_queue, envelope_body) - - -- Process immediately in Love2D main thread - self:flush() - - return true, "Envelope queued for sending in LÖVE 2D" -end - -function Love2DTransport:configure(config: table): transport_utils.Transport - local dsn = (config as any).dsn or "" - self.dsn_info = dsn_utils.parse_dsn(dsn) - self.endpoint = dsn_utils.build_ingest_url(self.dsn_info) - self.envelope_endpoint = dsn_utils.build_envelope_url(self.dsn_info) - self.timeout = (config as any).timeout or 30 - self.event_queue = {} - self.envelope_queue = {} - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-love2d/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(self.dsn_info) - } - self.envelope_headers = { - ["Content-Type"] = "application/x-sentry-envelope", - ["User-Agent"] = "sentry-lua-love2d/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(self.dsn_info) - } - - return self as transport_utils.Transport -end - --- Process queued events and envelopes using lua-https -function Love2DTransport:flush() - local love_global = rawget(_G, "love") - if not love_global then - return - end - - -- Try to load lua-https module - local https_ok, https = pcall(require, "https") - if not https_ok then - print("[Sentry] lua-https module not available: " .. tostring(https)) - return - end - - -- Send queued events - if #self.event_queue > 0 then - for _, event in ipairs(self.event_queue) do - local body = json.encode(event) - - local status = https.request(self.endpoint, { - method = "POST", - headers = self.headers, - data = body - }) - - if status == 200 then - print("[Sentry] Event sent successfully (status: " .. status .. ")") - else - print("[Sentry] Event send failed to " .. self.endpoint .. " (status: " .. tostring(status) .. ")") - end - end - self.event_queue = {} - end - - -- Send queued envelopes - if #self.envelope_queue > 0 then - for _, envelope_body in ipairs(self.envelope_queue) do - local status = https.request(self.envelope_endpoint, { - method = "POST", - headers = self.envelope_headers, - data = envelope_body - }) - - if status == 200 then - print("[Sentry] Envelope sent successfully (status: " .. status .. ")") - else - print("[Sentry] Envelope send failed to " .. self.envelope_endpoint .. " (status: " .. tostring(status) .. ")") - end - end - self.envelope_queue = {} - end -end - --- Clean shutdown -function Love2DTransport:close() - -- Final flush before closing - self:flush() -end - --- Create factory function -local function create_love2d_transport(config: table): transport_utils.Transport - local transport = Love2DTransport - return transport:configure(config) -end - --- Check if LÖVE 2D transport is available -local function is_love2d_available(): boolean - return rawget(_G, "love") ~= nil -end - --- Register this transport factory with high priority for LÖVE 2D -transport_utils.register_transport_factory({ - name = "love2d", - priority = 180, -- High priority in LÖVE 2D environment - create = create_love2d_transport, - is_available = is_love2d_available -}) - -return { - Love2DTransport = Love2DTransport, - create_love2d_transport = create_love2d_transport, - is_love2d_available = is_love2d_available -} \ No newline at end of file diff --git a/src/sentry/platforms/lua.lua b/src/sentry/platforms/lua.lua new file mode 100644 index 0000000..8489981 --- /dev/null +++ b/src/sentry/platforms/lua.lua @@ -0,0 +1,103 @@ +-- NOTE: This module depends on the standard Lua environment. It should only be loaded after verifying it is indeed on standard Lua + +local function ensure_https() + local ok, https = pcall(require, "ssl.https") + if ok then return https end + + -- Hacks to get the debugger to work on a mac + local lr_path = "~/.luarocks/share/lua/5.4/?.lua;~/.luarocks/share/lua/5.4/?/init.lua;/opt/homebrew/share/lua/5.4/?.lua;/opt/homebrew/share/lua/5.4/?/init.lua" + local lr_cpath = "~/.luarocks/lib/lua/5.4/?.so;/opt/homebrew/lib/lua/5.4/?.so" + + if not package.path:find("/.luarocks/share/lua/5.4", 1, true) then package.path = package.path .. ";" .. lr_path end + if not package.cpath:find("/.luarocks/lib/lua/5.4", 1, true) then package.cpath = package.cpath .. ";" .. lr_cpath end + + local ok2, https2 = pcall(require, "ssl.https") + if ok2 then return https2 end + + -- Last resort: print why (helps when the debugger’s runtime/ABI mismatches) + local msg = ("Cannot load ssl.https.\npackage.path=%s\npackage.cpath=%s"):format(package.path, package.cpath) + io.stderr:write(msg .. "\n") + return nil +end + +ensure_https() + +local function tz_suffix(t) + local z = os.date("%z", t or os.time()) or "+0000" + assert(type(z) == "string") + return string.format("%s%s:%s", z:sub(1, 1), z:sub(2, 3), z:sub(4, 5)) +end + +local function safe_require(n) + local ok, m = pcall(require, n) + return ok and m or nil +end +local https = safe_require("ssl.https") +if not https then error("https not available (install LuaSec: ssl.https)") end + +local ltn12 = safe_require("ltn12") +local socket = safe_require("socket") + +local M = { name = "lua" } + +function M.timestamp() + local now, sec, ms + -- Try socket.gettime() for millisecond precision + if socket and type(socket.gettime) == "function" then + now = socket.gettime() + sec = math.floor(now) + ms = math.floor((now - sec) * 1000 + 0.5) + if ms >= 1000 then + ms = 0 + sec = sec + 1 + end + else + -- Fallback to os.time() without millisecond precision + sec, ms = os.time(), 0 + end + local d = os.date("*t", sec) + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, ms, tz_suffix(sec)) +end + +local function set_timeout(lib, ms) + if lib and ms then lib.TIMEOUT = math.max(0.001, ms / 1000) end +end + +local function request_with_optional_headers(lib, url, method, headers, body, timeout_ms) + set_timeout(lib, timeout_ms) + headers = headers or {} + body = body or "" + + if ltn12 then + local chunks = {} + -- luacheck: no unused secondaries + ---@diagnostic disable-next-line: unused-local + local ok, code, resp_headers, status = lib.request({ + url = url, + method = method, + headers = headers, + source = ltn12.source.string(body), + sink = ltn12.sink.table(chunks), + }) + if not ok then return nil, tostring(code or "request failed: ") end + return true, tonumber(code), table.concat(chunks), resp_headers + end + + local resp_body, code, resp_headers = lib.request(url, body) + if not resp_body then return nil, tostring(code or "request failed") end + return true, tonumber(code), resp_body, resp_headers +end + +function M.http_post(url, body, headers, opts) + opts = opts or {} + return request_with_optional_headers(https, url, "POST", headers, body, opts.timeout_ms) +end + +function M.http_post_async(url, body, headers, opts, callback) + -- luacheck: no unused secondaries + ---@diagnostic disable-next-line: unused-local + local ok, status, resp_body, resp_headers = M.http_post(url, body, headers, opts) + if callback then callback(ok, status, resp_body) end +end + +return M diff --git a/src/sentry/platforms/nginx/os_detection.tl b/src/sentry/platforms/nginx/os_detection.tl deleted file mode 100644 index a4b275c..0000000 --- a/src/sentry/platforms/nginx/os_detection.tl +++ /dev/null @@ -1,36 +0,0 @@ -local os_utils = require("sentry.utils.os") -local OSInfo = os_utils.OSInfo - -local function detect_os(): OSInfo - if _G.ngx then - -- OpenResty/nginx runs on Linux typically, try to get OS info - local handle = io.popen("uname -s 2>/dev/null") - if handle then - local name = handle:read("*a") - handle:close() - if name then - name = name:gsub("\n", "") - local handle_version = io.popen("uname -r 2>/dev/null") - if handle_version then - local version = handle_version:read("*a") - handle_version:close() - version = version and version:gsub("\n", "") or "" - return { - name = name, - version = version - } - end - end - end - end - return nil -end - --- Register this detector -os_utils.register_detector({ - detect = detect_os -}) - -return { - detect_os = detect_os -} \ No newline at end of file diff --git a/src/sentry/platforms/nginx/transport.tl b/src/sentry/platforms/nginx/transport.tl deleted file mode 100644 index 127a69a..0000000 --- a/src/sentry/platforms/nginx/transport.tl +++ /dev/null @@ -1,75 +0,0 @@ --- Nginx/OpenResty transport using resty.http -local transport_utils = require("sentry.utils.transport") -local dsn_utils = require("sentry.utils.dsn") -local json = require("sentry.utils.json") -local http = require("sentry.utils.http") -local version = require("sentry.version") - -local record NginxTransport - endpoint: string - timeout: number - headers: {string: string} -end - -function NginxTransport:send(event: table): boolean, string - local body = json.encode(event) - - local request = { - url = self.endpoint, - method = "POST", - headers = self.headers, - body = body, - timeout = self.timeout - } - - local response = http.request(request) - - if response.success and response.status == 200 then - return true, "Event sent successfully" - else - local error_msg = response.error or "HTTP error: " .. tostring(response.status) - return false, error_msg - end -end - -function NginxTransport:configure(config: table): transport_utils.Transport - local dsn, err = dsn_utils.parse_dsn((config as any).dsn or "") - if err then - error("Invalid DSN: " .. err) - end - - self.dsn = dsn - self.endpoint = dsn_utils.build_ingest_url(dsn) - self.timeout = (config as any).timeout or 30 - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-nginx/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn) - } - return self as transport_utils.Transport -end - --- Create factory function -local function create_nginx_transport(config: table): transport_utils.Transport - local transport = NginxTransport - return transport:configure(config) -end - --- Check if nginx transport is available -local function is_nginx_available(): boolean - return _G.ngx ~= nil -end - --- Register this transport factory with high priority for nginx -transport_utils.register_transport_factory({ - name = "nginx", - priority = 190, -- High priority in nginx/OpenResty environment - create = create_nginx_transport, - is_available = is_nginx_available -}) - -return { - NginxTransport = NginxTransport, - create_nginx_transport = create_nginx_transport, - is_nginx_available = is_nginx_available -} \ No newline at end of file diff --git a/src/sentry/platforms/openresty.lua b/src/sentry/platforms/openresty.lua new file mode 100644 index 0000000..01693b5 --- /dev/null +++ b/src/sentry/platforms/openresty.lua @@ -0,0 +1,45 @@ +local M = { name = "openresty" } + +local function safe_require(n) + local ok, m = pcall(require, n) + return ok and m or nil +end +local http = safe_require("resty.http") +if not http then error("http not available") end + +function M.http_post(url, body, headers, opts) + if not http then return nil, "resty.http not available" end + local httpc = http.new() + if opts and opts.timeout_ms then httpc:set_timeout(opts.timeout_ms) end + local res, err = httpc:request_uri(url, { + method = "POST", + body = body or "", + headers = headers or {}, + keepalive = not (opts and opts.keepalive == false), + }) + if not res then return nil, err end + return true, res.status, res.body, res.headers +end + +function M.http_post_async(url, body, headers, opts, callback) + ---@diagnostic disable-next-line: undefined-global + ngx.thread.spawn(function() + local ok, status, resp_body = M.http_post(url, body, headers, opts) + if callback then callback(ok, status, resp_body) end + end) +end + +function M.timestamp() + ---@diagnostic disable-next-line: undefined-global + local now = ngx.now() -- seconds + fractional + local sec = math.floor(now) + local ms = math.floor((now - sec) * 1000 + 0.5) + if ms == 1000 then + ms = 0 + sec = sec + 1 + end + local d = os.date("!*t", sec) -- UTC + return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", d.year, d.month, d.day, d.hour, d.min, d.sec, ms) +end + +return M diff --git a/src/sentry/platforms/redis/transport.tl b/src/sentry/platforms/redis/transport.tl deleted file mode 100644 index 1ccc963..0000000 --- a/src/sentry/platforms/redis/transport.tl +++ /dev/null @@ -1,66 +0,0 @@ --- Redis transport for queuing events -local transport_utils = require("sentry.utils.transport") -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record RedisTransport - endpoint: string - timeout: number - headers: {string: string} - redis_key: string -end - -function RedisTransport:send(event: table): boolean, string - if not _G.redis then - return false, "Redis not available in this environment" - end - - local body = json.encode(event) - - local success, err = pcall(function() - (_G.redis as any).call("LPUSH", self.redis_key or "sentry:events", body) - (_G.redis as any).call("LTRIM", self.redis_key or "sentry:events", 0, 999) - end) - - if success then - return true, "Event queued in Redis" - else - return false, "Redis error: " .. tostring(err) - end -end - -function RedisTransport:configure(config: table): transport_utils.Transport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.redis_key = (config as any).redis_key or "sentry:events" - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-redis/" .. version - } - return self as transport_utils.Transport -end - --- Create factory function -local function create_redis_transport(config: table): transport_utils.Transport - local transport = RedisTransport - return transport:configure(config) -end - --- Check if redis transport is available -local function is_redis_available(): boolean - return _G.redis ~= nil -end - --- Register this transport factory with medium priority -transport_utils.register_transport_factory({ - name = "redis", - priority = 150, -- Medium priority for Redis environments - create = create_redis_transport, - is_available = is_redis_available -}) - -return { - RedisTransport = RedisTransport, - create_redis_transport = create_redis_transport, - is_redis_available = is_redis_available -} \ No newline at end of file diff --git a/src/sentry/platforms/roblox.lua b/src/sentry/platforms/roblox.lua new file mode 100644 index 0000000..6eaaf18 --- /dev/null +++ b/src/sentry/platforms/roblox.lua @@ -0,0 +1,51 @@ +-- NOTE: This module depends on the Roblox environment. It should only be loaded after verifying it is indeed on Roblox +---@diagnostic disable: undefined-global + +local HttpService = game:GetService("HttpService") + +local M = { name = "roblox" } + +function M.timestamp() + -- "2025-08-23T21:05:17.123Z" + return DateTime.now():ToIsoDate() +end + +function M.http_post(url, body, headers, _) + local ok, res = pcall(function() + return HttpService:RequestAsync({ + Url = url, + Method = "POST", + Headers = headers or {}, + Body = body or "", + }) + end) + if not ok then return nil, tostring(res) end + return res.Success, res.StatusCode, res.Body, res.Headers +end + +function M.http_post_async(url, body, headers, _, callback) + task.spawn(function() + local ok, res = pcall(function() + return HttpService:RequestAsync({ + Url = url, + Method = "POST", + Headers = headers or {}, + Body = body or "", + }) + end) + if callback then + if not ok then + callback(false, tostring(res)) + else + callback(res.Success, res.StatusCode, res.Body) + end + end + end) +end + +function M.sleep(ms) + task.wait((ms or 0) / 1000) + return true +end + +return M diff --git a/src/sentry/platforms/roblox/context.tl b/src/sentry/platforms/roblox/context.tl deleted file mode 100644 index 7770413..0000000 --- a/src/sentry/platforms/roblox/context.tl +++ /dev/null @@ -1,25 +0,0 @@ --- Roblox-specific context information -local function get_roblox_context(): table - local context = {} - - if _G.game then - local game = _G.game as any - context.game_id = game.GameId - context.place_id = game.PlaceId - context.job_id = game.JobId - end - - if _G.game and (_G.game as any).Players and (_G.game as any).Players.LocalPlayer then - local player = (_G.game as any).Players.LocalPlayer - context.player = { - name = player.Name, - user_id = player.UserId - } - end - - return context -end - -return { - get_roblox_context = get_roblox_context -} \ No newline at end of file diff --git a/src/sentry/platforms/roblox/file_io.tl b/src/sentry/platforms/roblox/file_io.tl deleted file mode 100644 index ab9d4c5..0000000 --- a/src/sentry/platforms/roblox/file_io.tl +++ /dev/null @@ -1,52 +0,0 @@ -local file_io = require("sentry.core.file_io") - -local record RobloxFileIO -end - -function RobloxFileIO:write_file(path: string, content: string): boolean, string - local success, err = pcall(function() - local DataStoreService = game:GetService("DataStoreService") - local datastore = DataStoreService:GetDataStore("SentryEvents") - - local timestamp = tostring(os.time()) - datastore:SetAsync(timestamp, content) - end) - - if success then - return true, "Event written to Roblox DataStore" - else - return false, "Roblox DataStore error: " .. tostring(err) - end -end - -function RobloxFileIO:read_file(path: string): string, string - local success, result = pcall(function() - local DataStoreService = game:GetService("DataStoreService") - local datastore = DataStoreService:GetDataStore("SentryEvents") - return datastore:GetAsync(path) - end) - - if success then - return result or "", "" - else - return "", "Failed to read from DataStore: " .. tostring(result) - end -end - -function RobloxFileIO:file_exists(path: string): boolean - local content, err = self:read_file(path) - return err == "" -end - -function RobloxFileIO:ensure_directory(path: string): boolean, string - return true, "Directories not needed for Roblox DataStore" -end - -local function create_roblox_file_io(): file_io.FileIO - return setmetatable({}, {__index = RobloxFileIO}) as file_io.FileIO -end - -return { - RobloxFileIO = RobloxFileIO, - create_roblox_file_io = create_roblox_file_io -} \ No newline at end of file diff --git a/src/sentry/platforms/roblox/os_detection.tl b/src/sentry/platforms/roblox/os_detection.tl deleted file mode 100644 index dcb118d..0000000 --- a/src/sentry/platforms/roblox/os_detection.tl +++ /dev/null @@ -1,23 +0,0 @@ -local os_utils = require("sentry.utils.os") -local OSInfo = os_utils.OSInfo - -local function detect_os(): OSInfo - -- In Roblox, we can't execute system commands - -- But we can detect we're in Roblox environment - if _G.game and _G.game.GetService then - return { - name = "Roblox", - version = nil - } - end - return nil -end - --- Register this detector -os_utils.register_detector({ - detect = detect_os -}) - -return { - detect_os = detect_os -} \ No newline at end of file diff --git a/src/sentry/platforms/roblox/transport.tl b/src/sentry/platforms/roblox/transport.tl deleted file mode 100644 index 5bdf259..0000000 --- a/src/sentry/platforms/roblox/transport.tl +++ /dev/null @@ -1,85 +0,0 @@ --- Roblox-specific transport using HttpService -local transport_utils = require("sentry.utils.transport") -local dsn_utils = require("sentry.utils.dsn") -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record RobloxTransport - endpoint: string - timeout: number - headers: {string: string} - dsn: dsn_utils.DSN -end - -function RobloxTransport:send(event: table): boolean, string - -- Check if we're in Roblox environment - if not _G.game then - return false, "Not in Roblox environment" - end - - local success_service, HttpService = pcall(function() - return (_G.game as any):GetService("HttpService") - end) - - if not success_service or not HttpService then - return false, "HttpService not available in Roblox" - end - - local body = json.encode(event) - - local success, response = pcall(function() - return HttpService:PostAsync(self.endpoint, body, - (_G as any).Enum.HttpContentType.ApplicationJson, - false, -- compress - self.headers - ) - end) - - if success then - return true, "Event sent via Roblox HttpService" - else - return false, "Roblox HTTP error: " .. tostring(response) - end -end - -function RobloxTransport:configure(config: table): transport_utils.Transport - local dsn, err = dsn_utils.parse_dsn((config as any).dsn or "") - if err then - error("Invalid DSN: " .. err) - end - - self.dsn = dsn - self.endpoint = dsn_utils.build_ingest_url(dsn) - self.timeout = (config as any).timeout or 30 - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-roblox/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn) - } - return self as transport_utils.Transport -end - --- Create factory function -local function create_roblox_transport(config: table): transport_utils.Transport - local transport = RobloxTransport - return transport:configure(config) -end - --- Check if Roblox transport is available -local function is_roblox_available(): boolean - return _G.game and (_G.game as any).GetService ~= nil -end - --- Register this transport factory with high priority for Roblox -transport_utils.register_transport_factory({ - name = "roblox", - priority = 200, -- Very high priority in Roblox environment - create = create_roblox_transport, - is_available = is_roblox_available -}) - -return { - RobloxTransport = RobloxTransport, - create_roblox_transport = create_roblox_transport, - is_roblox_available = is_roblox_available -} \ No newline at end of file diff --git a/src/sentry/platforms/standard/file_transport.tl b/src/sentry/platforms/standard/file_transport.tl deleted file mode 100644 index 8c699e5..0000000 --- a/src/sentry/platforms/standard/file_transport.tl +++ /dev/null @@ -1,89 +0,0 @@ --- File-based transport for environments without HTTP access -local transport_utils = require("sentry.utils.transport") -local file_io = require("sentry.core.file_io") -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record FileTransport - endpoint: string - timeout: number - headers: {string: string} - file_path: string - file_io: file_io.FileIO - append_mode: boolean -end - -function FileTransport:send(event: table): boolean, string - local serialized = json.encode(event) - local timestamp = os.date("%Y-%m-%d %H:%M:%S") - local content = string.format("[%s] %s\n", timestamp, serialized) - - if self.append_mode and self.file_io:file_exists(self.file_path) then - local existing_content, read_err = self.file_io:read_file(self.file_path) - if read_err ~= "" then - return false, "Failed to read existing file: " .. read_err - end - content = existing_content .. content - end - - local success, err = self.file_io:write_file(self.file_path, content) - - if success then - return true, "Event written to file: " .. self.file_path - else - return false, "Failed to write event: " .. err - end -end - -function FileTransport:configure(config: table): transport_utils.Transport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.file_path = (config as any).file_path or "sentry-events.log" - self.append_mode = (config as any).append_mode ~= false - - if (config as any).file_io then - self.file_io = (config as any).file_io - else - self.file_io = file_io.create_standard_file_io() - end - - local dir_path = self.file_path:match("^(.*/)") - if dir_path then - local dir_success, dir_err = self.file_io:ensure_directory(dir_path) - if not dir_success then - print("Warning: Failed to create directory: " .. dir_err) - end - end - - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-file/" .. version - } - - return self as transport_utils.Transport -end - --- Create factory function -local function create_file_transport(config: table): transport_utils.Transport - local transport = FileTransport - return transport:configure(config) -end - --- File transport is always available as a fallback -local function is_file_available(): boolean - return true -end - --- Register this transport factory with lower priority (fallback) -transport_utils.register_transport_factory({ - name = "file", - priority = 10, -- Low priority - used as fallback - create = create_file_transport, - is_available = is_file_available -}) - -return { - FileTransport = FileTransport, - create_file_transport = create_file_transport, - is_file_available = is_file_available -} \ No newline at end of file diff --git a/src/sentry/platforms/standard/os_detection.tl b/src/sentry/platforms/standard/os_detection.tl deleted file mode 100644 index 9c0551f..0000000 --- a/src/sentry/platforms/standard/os_detection.tl +++ /dev/null @@ -1,81 +0,0 @@ -local os_utils = require("sentry.utils.os") -local OSInfo = os_utils.OSInfo - -local function detect_os(): OSInfo - -- Try to detect OS through standard Lua mechanisms - local handle = io.popen("uname -s 2>/dev/null") - if handle then - local name = handle:read("*a") - handle:close() - if name then - name = name:gsub("\n", "") - if name ~= "" then - -- Special handling for Darwin -> macOS - if name == "Darwin" then - -- Check if this is actually macOS by looking for system version - local sw_vers = io.popen("sw_vers -productVersion 2>/dev/null") - if sw_vers then - local macos_version = sw_vers:read("*a") - sw_vers:close() - if macos_version and macos_version:gsub("\n", "") ~= "" then - return { - name = "macOS", - version = macos_version:gsub("\n", "") - } - end - end - -- Fallback to Darwin if sw_vers not available - name = "Darwin" - end - - -- Get version for non-Darwin systems - local version_handle = io.popen("uname -r 2>/dev/null") - if version_handle then - local version = version_handle:read("*a") - version_handle:close() - if version then - version = version:gsub("\n", "") - return { - name = name, - version = version - } - end - end - - return { - name = name, - version = nil - } - end - end - end - - -- Fallback: try to detect through package.config - local sep = package.config:sub(1,1) - if sep == "\\" then - -- Windows - local handle_win = io.popen("ver 2>nul") - if handle_win then - local output = handle_win:read("*a") - handle_win:close() - if output and output:match("Microsoft Windows") then - local version = output:match("%[Version ([^%]]+)%]") - return { - name = "Windows", - version = version or nil - } - end - end - end - - return nil -end - --- Register this detector -os_utils.register_detector({ - detect = detect_os -}) - -return { - detect_os = detect_os -} \ No newline at end of file diff --git a/src/sentry/platforms/standard/transport.tl b/src/sentry/platforms/standard/transport.tl deleted file mode 100644 index 09dbd48..0000000 --- a/src/sentry/platforms/standard/transport.tl +++ /dev/null @@ -1,104 +0,0 @@ --- Standard HTTP transport for desktop platforms (Standard Lua, LuaJIT) -local transport_utils = require("sentry.utils.transport") -local dsn_utils = require("sentry.utils.dsn") -local json = require("sentry.utils.json") -local http = require("sentry.utils.http") -local version = require("sentry.version") - -local record HttpTransport - endpoint: string - envelope_endpoint: string - timeout: number - headers: {string: string} - envelope_headers: {string: string} - dsn: dsn_utils.DSN -end - -function HttpTransport:send(event: table): boolean, string - local body = json.encode(event) - - local request = { - url = self.endpoint, - method = "POST", - headers = self.headers, - body = body, - timeout = self.timeout - } - - local response = http.request(request) - - if response.success and response.status == 200 then - return true, "Event sent successfully" - else - local error_msg = response.error or "Failed to send event: " .. tostring(response.status) - return false, error_msg - end -end - --- Send transaction as envelope -function HttpTransport:send_envelope(envelope_body: string): boolean, string - local request = { - url = self.envelope_endpoint, - method = "POST", - headers = self.envelope_headers, - body = envelope_body, - timeout = self.timeout - } - - local response = http.request(request) - - if response.success and response.status == 200 then - return true, "Envelope sent successfully" - else - local error_msg = response.error or "Failed to send envelope: " .. tostring(response.status) - return false, error_msg - end -end - -function HttpTransport:configure(config: table): transport_utils.Transport - local dsn, err = dsn_utils.parse_dsn((config as any).dsn or "") - if err then - error("Invalid DSN: " .. err) - end - - self.dsn = dsn - self.endpoint = dsn_utils.build_ingest_url(dsn) - self.envelope_endpoint = dsn_utils.build_envelope_url(dsn) - self.timeout = (config as any).timeout or 30 - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn) - } - self.envelope_headers = { - ["Content-Type"] = "application/x-sentry-envelope", - ["User-Agent"] = "sentry-lua/" .. version, - ["X-Sentry-Auth"] = dsn_utils.build_auth_header(dsn) - } - return self as transport_utils.Transport -end - --- Create factory function -local function create_http_transport(config: table): transport_utils.Transport - local transport = HttpTransport - return transport:configure(config) -end - --- Check if HTTP transport is available -local function is_http_available(): boolean - return http.available -end - --- Register this transport factory -transport_utils.register_transport_factory({ - name = "standard-http", - priority = 100, -- High priority for standard environments - create = create_http_transport, - is_available = is_http_available -}) - -return { - HttpTransport = HttpTransport, - create_http_transport = create_http_transport, - is_http_available = is_http_available -} \ No newline at end of file diff --git a/src/sentry/platforms/test/transport.tl b/src/sentry/platforms/test/transport.tl deleted file mode 100644 index 242ca87..0000000 --- a/src/sentry/platforms/test/transport.tl +++ /dev/null @@ -1,59 +0,0 @@ --- Test transport for capturing events in memory -local transport_utils = require("sentry.utils.transport") -local version = require("sentry.version") - -local record TestTransport - endpoint: string - timeout: number - headers: {string: string} - events: {table} -end - -function TestTransport:send(event: table): boolean, string - table.insert(self.events, event) - return true, "Event captured in test transport" -end - -function TestTransport:configure(config: table): transport_utils.Transport - self.endpoint = (config as any).dsn or "" - self.timeout = (config as any).timeout or 30 - self.headers = { - ["Content-Type"] = "application/json", - ["User-Agent"] = "sentry-lua-test/" .. version - } - self.events = {} - return self as transport_utils.Transport -end - -function TestTransport:get_events(): {table} - return self.events -end - -function TestTransport:clear_events() - self.events = {} -end - --- Create factory function -local function create_test_transport(config: table): transport_utils.Transport - local transport = TestTransport - return transport:configure(config) -end - --- Test transport is always available (good for testing) -local function is_test_available(): boolean - return true -end - --- Register this transport factory with very low priority (testing only) -transport_utils.register_transport_factory({ - name = "test", - priority = 1, -- Very low priority - only used when explicitly requested - create = create_test_transport, - is_available = is_test_available -}) - -return { - TestTransport = TestTransport, - create_test_transport = create_test_transport, - is_test_available = is_test_available -} \ No newline at end of file diff --git a/src/sentry/tracing/headers.tl b/src/sentry/tracing/headers.tl deleted file mode 100644 index ff6fc14..0000000 --- a/src/sentry/tracing/headers.tl +++ /dev/null @@ -1,342 +0,0 @@ ----@class sentry.tracing.headers ---- Core functionality for parsing and generating Sentry distributed tracing headers ---- Implements sentry-trace header format: {trace_id}-{span_id}-{sampled} ---- Supports both incoming trace continuation and outgoing trace propagation - -local record TraceData - trace_id: string - span_id: string - sampled: boolean -end - - -local record TraceInfo - sentry_trace: TraceData - baggage: {string: string} - traceparent: string -end - -local record InjectOptions - include_traceparent: boolean -end - -local record Headers - parse_sentry_trace: function(header_value: string): TraceData - generate_sentry_trace: function(trace_data: TraceData): string - parse_baggage: function(header_value: string): {string: string} - generate_baggage: function(baggage_data: {string: string}): string - generate_trace_id: function(): string - generate_span_id: function(): string - extract_trace_headers: function(http_headers: {string: string}): TraceInfo - inject_trace_headers: function(http_headers: {string: string}, trace_data: TraceData, baggage_data: {string: string}, options: InjectOptions) -end - -local headers: Headers = {} - -local utils = require("sentry.utils") - --- Constants for header parsing and validation -local TRACE_ID_LENGTH: number = 32 -- 128-bit trace ID as hex string -local SPAN_ID_LENGTH: number = 16 -- 64-bit span ID as hex string - ----Parse sentry-trace header value ----@param header_value string? The sentry-trace header value ----@return table|nil trace_data Parsed trace data with trace_id, span_id, and sampled fields -function headers.parse_sentry_trace(header_value: string): TraceData - if not header_value or type(header_value) ~= "string" then - return nil - end - - -- Remove whitespace - local trimmed = header_value:match("^%s*(.-)%s*$") - if not trimmed then - return nil - end - header_value = trimmed - - if #header_value == 0 then - return nil - end - - -- Split by dashes: {trace_id}-{span_id}-{sampled} - local parts: {string} = {} - for part in header_value:gmatch("[^%-]+") do - table.insert(parts, part) - end - - -- Must have at least trace_id and span_id - if #parts < 2 then - return nil - end - - local trace_id = parts[1] - local span_id = parts[2] - local sampled = parts[3] -- Optional - - -- Validate trace_id (32 hex characters) - if not trace_id or #trace_id ~= TRACE_ID_LENGTH then - return nil - end - - if not trace_id:match("^[0-9a-fA-F]+$") then - return nil - end - - -- Validate span_id (16 hex characters) - if not span_id or #span_id ~= SPAN_ID_LENGTH then - return nil - end - - if not span_id:match("^[0-9a-fA-F]+$") then - return nil - end - - -- Parse sampled flag (optional) - local parsed_sampled: boolean = nil - if sampled then - if sampled == "1" then - parsed_sampled = true - elseif sampled == "0" then - parsed_sampled = false - else - -- Invalid sampled value, ignore it (defer sampling decision) - parsed_sampled = nil - end - end - - return { - trace_id = trace_id:lower(), -- Normalize to lowercase - span_id = span_id:lower(), - sampled = parsed_sampled - } -end - ----Generate sentry-trace header value from trace data ----@param trace_data table Trace data with trace_id, span_id, and optional sampled fields ----@return string|nil header_value The sentry-trace header value or nil if invalid input -function headers.generate_sentry_trace(trace_data: TraceData): string - if not trace_data or type(trace_data) ~= "table" then - return nil - end - - local trace_id = trace_data.trace_id - local span_id = trace_data.span_id - local sampled = trace_data.sampled - - -- Validate required fields - if not trace_id or not span_id then - return nil - end - - -- Validate trace_id format - if type(trace_id) ~= "string" or #trace_id ~= TRACE_ID_LENGTH then - return nil - end - - if not trace_id:match("^[0-9a-fA-F]+$") then - return nil - end - - -- Validate span_id format - if type(span_id) ~= "string" or #span_id ~= SPAN_ID_LENGTH then - return nil - end - - if not span_id:match("^[0-9a-fA-F]+$") then - return nil - end - - -- Build header value - local header_value = trace_id:lower() .. "-" .. span_id:lower() - - -- Add sampled flag if specified - if sampled == true then - header_value = header_value .. "-1" - elseif sampled == false then - header_value = header_value .. "-0" - end - -- If sampled is nil, defer sampling decision (no sampled flag) - - return header_value -end - ----Parse baggage header value ----Baggage format: key1=value1,key2=value2,key3=value3;property=value ----@param header_value string? The baggage header value ----@return table baggage_data Parsed baggage data as key-value pairs -function headers.parse_baggage(header_value: string): {string: string} - local baggage_data: {string: string} = {} - - if not header_value or type(header_value) ~= "string" then - return baggage_data - end - - -- Remove whitespace - local trimmed = header_value:match("^%s*(.-)%s*$") - if not trimmed then - return baggage_data - end - header_value = trimmed - - if #header_value == 0 then - return baggage_data - end - - -- Split by comma to get individual baggage items - for item in header_value:gmatch("[^,]+") do - local trimmed_item = item:match("^%s*(.-)%s*$") -- Trim whitespace - if trimmed_item then - item = trimmed_item - end - - -- Split by semicolon to separate key=value from properties - local key_value_part = item:match("([^;]*)") - if key_value_part then - local trimmed_kvp = key_value_part:match("^%s*(.-)%s*$") - if trimmed_kvp then - key_value_part = trimmed_kvp - end - - -- Split key=value - local key, value = key_value_part:match("^([^=]+)=(.*)$") - if key and value then - local trimmed_key = key:match("^%s*(.-)%s*$") -- Trim key - local trimmed_value = value:match("^%s*(.-)%s*$") -- Trim value - - -- URL decode if needed (basic implementation) - if trimmed_key and trimmed_value and #trimmed_key > 0 then - baggage_data[trimmed_key] = trimmed_value - end - end - end - end - - return baggage_data -end - ----Generate baggage header value from baggage data ----@param baggage_data table Baggage data as key-value pairs ----@return string|nil header_value The baggage header value or nil if empty -function headers.generate_baggage(baggage_data: {string: string}): string - if not baggage_data or type(baggage_data) ~= "table" then - return nil - end - - local items: {string} = {} - for key, value in pairs(baggage_data) do - if type(key) == "string" and type(value) == "string" then - -- Basic URL encoding for special characters - local encoded_value = value:gsub("([,;=%%])", function(c: string): string - return string.format("%%%02X", string.byte(c)) - end) - - table.insert(items, key .. "=" .. encoded_value) - end - end - - if #items == 0 then - return nil - end - - return table.concat(items, ",") -end - ----Generate random trace ID (128-bit as 32 hex characters) ----@return string trace_id Random trace ID -function headers.generate_trace_id(): string - local uuid_result = utils.generate_uuid():gsub("-", "") - return uuid_result -end - ----Generate random span ID (64-bit as 16 hex characters) ----@return string span_id Random span ID -function headers.generate_span_id(): string - local uuid_result = utils.generate_uuid():gsub("-", ""):sub(1, 16) - return uuid_result -end - ----Extract trace propagation headers from HTTP headers table ----@param http_headers table HTTP headers as key-value pairs (case-insensitive lookup) ----@return table trace_info Extracted trace information -function headers.extract_trace_headers(http_headers: {string: string}): TraceInfo - if not http_headers or type(http_headers) ~= "table" then - return {} - end - - -- Case-insensitive header lookup - local function get_header(name: string): string - local name_lower = name:lower() - for key, value in pairs(http_headers) do - if type(key) == "string" and key:lower() == name_lower then - return value - end - end - return nil - end - - local trace_info: TraceInfo = {} - - -- Extract sentry-trace header - local sentry_trace = get_header("sentry-trace") - if sentry_trace then - trace_info.sentry_trace = headers.parse_sentry_trace(sentry_trace) - end - - -- Extract baggage header - local baggage = get_header("baggage") - if baggage then - trace_info.baggage = headers.parse_baggage(baggage) - end - - -- Extract traceparent header (W3C Trace Context) - local traceparent = get_header("traceparent") - if traceparent then - trace_info.traceparent = traceparent - end - - return trace_info -end - ----Inject trace propagation headers into HTTP headers table ----@param http_headers table HTTP headers table to modify ----@param trace_data table Trace data with trace_id, span_id, and optional sampled ----@param baggage_data table? Optional baggage data ----@param options table? Options for header injection -function headers.inject_trace_headers(http_headers: {string: string}, trace_data: TraceData, baggage_data: {string: string}, options: InjectOptions) - if not http_headers or type(http_headers) ~= "table" then - return - end - - if not trace_data or type(trace_data) ~= "table" then - return - end - - options = options or {} - - -- Inject sentry-trace header - local sentry_trace = headers.generate_sentry_trace(trace_data) - if sentry_trace then - http_headers["sentry-trace"] = sentry_trace - end - - -- Inject baggage header if provided - if baggage_data then - local baggage = headers.generate_baggage(baggage_data) - if baggage then - http_headers["baggage"] = baggage - end - end - - -- Inject traceparent header if requested (for OpenTelemetry interop) - if options.include_traceparent and trace_data.trace_id and trace_data.span_id then - -- W3C traceparent format: 00-{trace_id}-{span_id}-{flags} - local flags = "00" -- Default flags - if trace_data.sampled == true then - flags = "01" - end - - http_headers["traceparent"] = "00-" .. trace_data.trace_id .. "-" .. trace_data.span_id .. "-" .. flags - end -end - -return headers \ No newline at end of file diff --git a/src/sentry/tracing/init.tl b/src/sentry/tracing/init.tl deleted file mode 100644 index 4e45d55..0000000 --- a/src/sentry/tracing/init.tl +++ /dev/null @@ -1,322 +0,0 @@ ----@class sentry.tracing ---- Main tracing module for Sentry Lua SDK ---- Provides distributed tracing functionality with "Tracing without Performance" (TwP) mode by default ---- Handles trace continuation, propagation, and integration with HTTP clients/servers - -local record TracingConfig - trace_propagation_targets: {string} - include_traceparent: boolean - baggage_keys: {string} -end - -local record TraceContext - trace_id: string - span_id: string - parent_span_id: string -end - -local record TraceInfo - trace_id: string - span_id: string - parent_span_id: string - sampled: boolean - is_tracing_enabled: boolean -end - -local record IdPair - trace_id: string - span_id: string -end - -local record Tracing - headers: any - propagation: any - performance: any - _config: TracingConfig - - init: function(config: TracingConfig) - continue_trace_from_request: function(request_headers: {string: string}): TraceContext - get_request_headers: function(target_url: string): {string: string} - start_trace: function(options: {string: any}): TraceContext - start_transaction: function(name: string, op: string, options: {string: any}): any - finish_transaction: function(status: string) - start_span: function(op: string, description: string, options: {string: any}): any - finish_span: function(status: string) - create_child: function(options: {string: any}): TraceContext - get_current_trace_info: function(): TraceInfo - is_active: function(): boolean - clear: function() - attach_trace_context_to_event: function(event: {string: any}): {string: any} - get_envelope_trace_header: function(): {string: string} - wrap_http_request: function(http_client: function(string, {string: any}): any, url: string, options: {string: any}): any - wrap_http_handler: function(handler: function(any, any): any): function(any, any): any - generate_ids: function(): IdPair -end - -local tracing: Tracing = {} - -local headers = require("sentry.tracing.headers") -local propagation = require("sentry.tracing.propagation") - --- Load performance monitoring if available -local performance: any = nil -local has_performance, perf_module = pcall(require, "sentry.performance") -if has_performance then - performance = perf_module -end - --- Re-export core functionality -tracing.headers = headers -tracing.propagation = propagation -if performance then - tracing.performance = performance -end - ----Initialize tracing for the SDK ----@param config TracingConfig? Optional tracing configuration -function tracing.init(config: TracingConfig) - config = config or {} - - -- Store config for later use - tracing._config = config - - -- Initialize propagation context if not already present - local current = propagation.get_current_context() - if not current then - propagation.start_new_trace() - end -end - ----Continue trace from incoming HTTP request headers ----This should be called at the beginning of request handling ----@param request_headers table HTTP headers from incoming request ----@return table trace_context The trace context created/continued -function tracing.continue_trace_from_request(request_headers: {string: string}): TraceContext - local context = propagation.continue_trace_from_headers(request_headers) - - -- Attach trace context to current scope for events - local trace_context = propagation.get_trace_context_for_event() - if trace_context then - -- In a real implementation, this would integrate with the scope system - -- sentry.get_current_scope():set_context("trace", trace_context) - end - - return trace_context as TraceContext -end - ----Get headers to add to outgoing HTTP requests ----This should be called before making outgoing HTTP requests ----@param target_url string? The URL being requested (for trace propagation targeting) ----@return table headers HTTP headers to add to the request -function tracing.get_request_headers(target_url: string): {string: string} - local config = tracing._config or {} - - local options = { - trace_propagation_targets = config.trace_propagation_targets, - include_traceparent = config.include_traceparent - } - - return propagation.get_trace_headers_for_request(target_url, options) -end - ----Start a new trace manually ----@param options table? Options for the new trace ----@return table trace_context The new trace context -function tracing.start_trace(options: {string: any}): TraceContext - local context = propagation.start_new_trace(options) - return propagation.get_trace_context_for_event() as TraceContext -end - ----Start a new transaction with performance timing ----@param name string Transaction name ----@param op string Operation type (e.g., "http.server", "navigation") ----@param options table? Additional options ----@return table transaction The started transaction -function tracing.start_transaction(name: string, op: string, options: {string: any}): any - options = options or {} - - -- If performance monitoring is available, use it - if performance then - -- Get current trace context for transaction - local trace_context = propagation.get_current_context() - if trace_context then - options.trace_id = trace_context.trace_id - options.parent_span_id = trace_context.span_id - end - - return performance.start_transaction(name, op, options) - end - - -- Fallback: just start a trace without performance timing - return tracing.start_trace(options) -end - ----Finish the current transaction ----@param status string? Final status (default: "ok") -function tracing.finish_transaction(status: string) - if performance then - performance.finish_transaction(status) - end -end - ----Start a span within the current transaction ----@param op string Operation type (e.g., "db.query", "http.client") ----@param description string Span description ----@param options table? Additional options ----@return table span The started span -function tracing.start_span(op: string, description: string, options: {string: any}): any - if performance then - return performance.start_span(op, description, options) - end - - -- Fallback: create child context without timing - return tracing.create_child(options) -end - ----Finish the most recent span ----@param status string? Final status (default: "ok") -function tracing.finish_span(status: string) - if performance then - performance.finish_span(status) - end -end - ----Create a child span/operation (returns new context but doesn't change current) ----@param options table? Options for the child operation ----@return table child_context The child trace context -function tracing.create_child(options: {string: any}): TraceContext - local child_context = propagation.create_child_context(options) - return { - trace_id = child_context.trace_id, - span_id = child_context.span_id, - parent_span_id = child_context.parent_span_id - } -end - ----Get current trace information for debugging/logging ----@return table? trace_info Current trace information or nil if no active trace -function tracing.get_current_trace_info(): TraceInfo - local context = propagation.get_current_context() - if not context then - return nil - end - - return { - trace_id = context.trace_id, - span_id = context.span_id, - parent_span_id = context.parent_span_id, - sampled = context.sampled, - is_tracing_enabled = propagation.is_tracing_enabled() - } -end - ----Check if tracing is currently active ----@return boolean active True if tracing context is active -function tracing.is_active(): boolean - return propagation.is_tracing_enabled() -end - ----Clear current trace context -function tracing.clear() - propagation.clear_context() - tracing._config = nil -end - ----Attach trace context to an event (for error reporting, etc.) ----@param event table The event to modify ----@return table event The modified event with trace context -function tracing.attach_trace_context_to_event(event: {string: any}): {string: any} - if not event or type(event) ~= "table" then - return event - end - - local trace_context = propagation.get_trace_context_for_event() - if trace_context then - event.contexts = event.contexts or {} - event.contexts.trace = trace_context - end - - return event -end - ----Get dynamic sampling context for envelope headers ----@return table? dsc Dynamic sampling context for envelope header or nil -function tracing.get_envelope_trace_header(): {string: string} - return propagation.get_dynamic_sampling_context() -end - ----High-level wrapper for HTTP client requests with automatic header injection ----@param http_client function HTTP client function that accepts (url, options) ----@param url string The URL to request ----@param options table? Request options (headers will be merged) ----@return any result The result from the HTTP client function -function tracing.wrap_http_request(http_client: function(string, {string: any}): any, url: string, options: {string: any}): any - if type(http_client) ~= "function" then - error("http_client must be a function") - end - - options = options or {} - options.headers = options.headers or {} - - -- Add trace headers to the request - local trace_headers = tracing.get_request_headers(url) - for key, value in pairs(trace_headers) do - options.headers[key] = value - end - - -- Call the original HTTP client function - return http_client(url, options) -end - ----High-level wrapper for HTTP server request handling with automatic trace continuation ----@param handler function Request handler function that accepts (request, response) ----@return function wrapped_handler Wrapped handler that continues traces -function tracing.wrap_http_handler(handler: function(any, any): any): function(any, any): any - if type(handler) ~= "function" then - error("handler must be a function") - end - - return function(request: any, response: any): any - -- Continue trace from request headers - local request_headers: {string: string} = {} - - -- Extract headers from request object (format varies by HTTP library) - if request and request.headers then - request_headers = request.headers as {string: string} - elseif request and request.get_header then - -- Some libraries use methods to access headers - local get_header_fn = request.get_header as function(any, string): string - request_headers["sentry-trace"] = get_header_fn(request, "sentry-trace") - request_headers["baggage"] = get_header_fn(request, "baggage") - request_headers["traceparent"] = get_header_fn(request, "traceparent") - end - - -- Continue the trace - tracing.continue_trace_from_request(request_headers) - - -- Store original context to restore later - local original_context = propagation.get_current_context() - - local success, result = pcall(handler, request, response) - - -- Restore original context - propagation.set_current_context(original_context) - - if not success then - error(result) - end - - return result - end -end - ----Utility function to generate new trace and span IDs ----@return table ids Table with trace_id and span_id -function tracing.generate_ids(): IdPair - return { - trace_id = headers.generate_trace_id(), - span_id = headers.generate_span_id() - } -end - -return tracing \ No newline at end of file diff --git a/src/sentry/tracing/propagation.tl b/src/sentry/tracing/propagation.tl deleted file mode 100644 index f8c888e..0000000 --- a/src/sentry/tracing/propagation.tl +++ /dev/null @@ -1,313 +0,0 @@ ----@class sentry.tracing.propagation ---- Core tracing propagation context implementation ---- Implements "Tracing without Performance" (TwP) mode by default ---- Handles trace continuation from incoming headers and propagation to outgoing requests - -local record PropagationContext - trace_id: string - span_id: string - parent_span_id: string - sampled: boolean - baggage: {string: string} - dynamic_sampling_context: {string: string} -end - - -local record TracePropagationOptions - trace_propagation_targets: {string} - include_traceparent: boolean -end - -local record TraceOptions - baggage: {string: string} -end - -local record Propagation - create_context: function(trace_data: TraceData, baggage_data: {string: string}): PropagationContext - populate_dynamic_sampling_context: function(context: PropagationContext) - get_current_context: function(): PropagationContext - set_current_context: function(context: PropagationContext) - continue_trace_from_headers: function(http_headers: {string: string}): PropagationContext - get_trace_headers_for_request: function(target_url: string, options: TracePropagationOptions): {string: string} - get_trace_context_for_event: function(): {string: any} - get_dynamic_sampling_context: function(): {string: string} - start_new_trace: function(options: TraceOptions): PropagationContext - clear_context: function() - create_child_context: function(options: TraceOptions): PropagationContext - is_tracing_enabled: function(): boolean - get_current_trace_id: function(): string - get_current_span_id: function(): string -end - -local propagation: Propagation = {} - -local record TraceData - trace_id: string - span_id: string - sampled: boolean -end - -local headers = require("sentry.tracing.headers") - --- Current propagation context (stored per scope) -local current_context: PropagationContext = nil - ----Initialize a new propagation context ----@param trace_data table? Optional incoming trace data from headers ----@param baggage_data table? Optional baggage data ----@return PropagationContext context New propagation context -function propagation.create_context(trace_data: TraceData, baggage_data: {string: string}): PropagationContext - local context: PropagationContext = { - trace_id = "", - span_id = "", - parent_span_id = nil, - sampled = nil, - baggage = {}, - dynamic_sampling_context = {} - } - - if trace_data then - -- Continue incoming trace - context.trace_id = trace_data.trace_id - context.parent_span_id = trace_data.span_id - context.span_id = headers.generate_span_id() -- Generate new span ID - context.sampled = trace_data.sampled - else - -- Start new trace (TwP mode - defer sampling decision) - context.trace_id = headers.generate_trace_id() - context.span_id = headers.generate_span_id() - context.parent_span_id = nil - context.sampled = nil -- Deferred sampling decision - end - - context.baggage = baggage_data or {} - context.dynamic_sampling_context = {} - - -- Populate dynamic sampling context lazily - propagation.populate_dynamic_sampling_context(context) - - return context -end - ----Populate dynamic sampling context (DSC) for the trace ----@param context PropagationContext The propagation context to populate -function propagation.populate_dynamic_sampling_context(context: PropagationContext) - if not context or not context.trace_id then - return - end - - local dsc = context.dynamic_sampling_context - - -- Add trace ID to DSC - dsc["sentry-trace_id"] = context.trace_id - - -- Add public key (would come from SDK configuration) - -- dsc["sentry-public_key"] = sdk_config.public_key - - -- Add environment if available - -- dsc["sentry-environment"] = sdk_config.environment - - -- Add release if available - -- dsc["sentry-release"] = sdk_config.release - - -- Note: We don't add sentry-sampled in TwP mode since sampling is deferred - -- This will be added when regular tracing (with spans) is enabled -end - ----Get the current propagation context ----@return PropagationContext? context Current propagation context or nil -function propagation.get_current_context(): PropagationContext - return current_context -end - ----Set the current propagation context ----@param context PropagationContext? The propagation context to set -function propagation.set_current_context(context: PropagationContext) - current_context = context -end - ----Continue trace from incoming HTTP headers ----@param http_headers table HTTP headers from incoming request ----@return PropagationContext context New propagation context continuing the trace -function propagation.continue_trace_from_headers(http_headers: {string: string}): PropagationContext - local trace_info = headers.extract_trace_headers(http_headers) - - local trace_data: TraceData = nil - local baggage_data = trace_info.baggage or {} - - -- Priority: sentry-trace > traceparent (W3C) - if trace_info.sentry_trace then - local sentry_trace_data = trace_info.sentry_trace - trace_data = { - trace_id = sentry_trace_data.trace_id, - span_id = sentry_trace_data.span_id, - sampled = sentry_trace_data.sampled - } - elseif trace_info.traceparent then - -- Parse W3C traceparent: 00-{trace_id}-{span_id}-{flags} - local version, trace_id, span_id, flags = trace_info.traceparent:match("^([0-9a-fA-F][0-9a-fA-F])%-([0-9a-fA-F]+)%-([0-9a-fA-F]+)%-([0-9a-fA-F][0-9a-fA-F])$") - if version == "00" and trace_id and span_id and #trace_id == 32 and #span_id == 16 then - trace_data = { - trace_id = trace_id, - span_id = span_id, - sampled = (tonumber(flags, 16) or 0) > 0 and true or nil - } - end - end - - local context = propagation.create_context(trace_data, baggage_data) - propagation.set_current_context(context) - - return context -end - ----Get trace headers for outgoing HTTP requests ----@param target_url string? Optional target URL for trace propagation targeting ----@param options table? Options for header generation ----@return table headers HTTP headers to add to outgoing request -function propagation.get_trace_headers_for_request(target_url: string, options: TracePropagationOptions): {string: string} - local context = propagation.get_current_context() - if not context then - return {} - end - - options = options or {} - local result_headers: {string: string} = {} - - -- Check trace propagation targets (simplified - would use real target matching) - local should_propagate = true - if options.trace_propagation_targets then - should_propagate = false - for _, target in ipairs(options.trace_propagation_targets) do - if target == "*" then - -- Wildcard - propagate to all targets - should_propagate = true - break - elseif target_url and target_url:find(target) then - -- Pattern match (supports Lua patterns like example%.com) - should_propagate = true - break - end - end - end - - if not should_propagate then - return {} - end - - -- Generate trace data for propagation - local trace_data: TraceData = { - trace_id = context.trace_id, - span_id = context.span_id, - sampled = context.sampled - } - - -- Inject headers (cast to headers TraceData type) - local headers_trace_data = { - trace_id = trace_data.trace_id, - span_id = trace_data.span_id, - sampled = trace_data.sampled - } - headers.inject_trace_headers(result_headers, headers_trace_data, context.baggage, { - include_traceparent = options.include_traceparent - }) - - return result_headers -end - ----Create trace context for attaching to events ----@return table? trace_context Trace context for event.contexts.trace or nil -function propagation.get_trace_context_for_event(): {string: any} - local context = propagation.get_current_context() - if not context or not context.trace_id then - return nil - end - - return { - trace_id = context.trace_id, - span_id = context.span_id, - parent_span_id = context.parent_span_id, - -- Note: In TwP mode, we don't have actual span data like op, description, etc. - } -end - ----Get dynamic sampling context for envelope headers ----@return table? dsc Dynamic sampling context or nil -function propagation.get_dynamic_sampling_context(): {string: string} - local context = propagation.get_current_context() - if not context or not context.dynamic_sampling_context then - return nil - end - - -- Return copy to avoid mutations - local dsc: {string: string} = {} - for k, v in pairs(context.dynamic_sampling_context) do - dsc[k] = v - end - - return dsc -end - ----Start a new trace (reset propagation context) ----@param options table? Options for the new trace ----@return PropagationContext context New propagation context -function propagation.start_new_trace(options: TraceOptions): PropagationContext - options = options or {} - - local context = propagation.create_context(nil, options.baggage) - propagation.set_current_context(context) - - return context -end - ----Clear current propagation context -function propagation.clear_context() - current_context = nil -end - ----Create a child context (for creating child spans/operations) ----@param options table? Options for the child context ----@return PropagationContext context New child propagation context -function propagation.create_child_context(options: TraceOptions): PropagationContext - local parent_context = propagation.get_current_context() - if not parent_context then - -- No parent context, start new trace - return propagation.start_new_trace(options) - end - - options = options or {} - - local child_context: PropagationContext = { - trace_id = parent_context.trace_id, - span_id = headers.generate_span_id(), -- New span ID for child - parent_span_id = parent_context.span_id, - sampled = parent_context.sampled, - baggage = parent_context.baggage, - dynamic_sampling_context = parent_context.dynamic_sampling_context - } - - return child_context -end - ----Check if tracing is enabled (has active trace context) ----@return boolean enabled True if tracing context is active -function propagation.is_tracing_enabled(): boolean - local context = propagation.get_current_context() - return context ~= nil and context.trace_id ~= nil -end - ----Get trace ID from current context ----@return string? trace_id Current trace ID or nil -function propagation.get_current_trace_id(): string - local context = propagation.get_current_context() - return context and context.trace_id or nil -end - ----Get span ID from current context ----@return string? span_id Current span ID or nil -function propagation.get_current_span_id(): string - local context = propagation.get_current_context() - return context and context.span_id or nil -end - -return propagation \ No newline at end of file diff --git a/src/sentry/types.tl b/src/sentry/types.tl deleted file mode 100644 index feb3e1c..0000000 --- a/src/sentry/types.tl +++ /dev/null @@ -1,118 +0,0 @@ --- Core types for Sentry SDK - --- Sentry initialization options -local record SentryOptions - dsn: string - environment: string - release: string - debug: boolean - sample_rate: number - max_breadcrumbs: number - before_send: function(event: table): table - transport: any - test_transport: boolean - file_transport: boolean - file_path: string - append_mode: boolean -end - --- User information -local record User - id: string - email: string - username: string - ip_address: string - segment: string -end - --- Breadcrumb data -local record Breadcrumb - timestamp: number - message: string - category: string - level: string - data: {string: any} -end - --- Context information (runtime, OS, device, etc.) -local record RuntimeContext - name: string - version: string - description: string -end - -local record OSContext - name: string - version: string - build: string - kernel_version: string -end - -local record DeviceContext - name: string - family: string - model: string - model_id: string - arch: string - battery_level: number - orientation: string - manufacturer: string - brand: string -end - --- Stack frame information -local record StackFrame - filename: string - ["function"]: string -- Sentry expects "function", not "function_name" - lineno: number - in_app: boolean - vars: {string: any} -- local variables and parameters -end - -local record StackTrace - frames: {StackFrame} -end - --- Exception information -local record Exception - type: string - value: string - module: string - thread_id: string - stacktrace: StackTrace -end - --- Event data structure -local record EventData - event_id: string - timestamp: number - level: string - logger: string - platform: string - sdk: {string: any} - message: string - exception: table - stacktrace: StackTrace - user: User - tags: {string: string} - extra: {string: any} - breadcrumbs: {Breadcrumb} - environment: string - release: string - contexts: {string: any} -end - -local types = { - SentryOptions = SentryOptions, - User = User, - Breadcrumb = Breadcrumb, - RuntimeContext = RuntimeContext, - OSContext = OSContext, - DeviceContext = DeviceContext, - StackFrame = StackFrame, - StackTrace = StackTrace, - Exception = Exception, - EventData = EventData -} - -return types \ No newline at end of file diff --git a/src/sentry/utils.tl b/src/sentry/utils.tl deleted file mode 100644 index 21521c6..0000000 --- a/src/sentry/utils.tl +++ /dev/null @@ -1,177 +0,0 @@ ----@class sentry.utils ---- Utility functions for the Sentry Lua SDK ---- Provides helper functions for ID generation, encoding, etc. - -local record Utils - _random_seeded: boolean - - generate_uuid: function(): string - generate_hex: function(length: number): string - url_encode: function(str: string): string - url_decode: function(str: string): string - is_empty: function(str: string): boolean - trim: function(str: string): string - deep_copy: function(orig: T): T - merge_tables: function(t1: {K: V}, t2: {K: V}): {K: V} - get_timestamp: function(): number - get_timestamp_ms: function(): number -end - -local utils: Utils = {} - ----Generate a random UUID (version 4) ----@return string uuid A random UUID in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx -function utils.generate_uuid(): string - -- Seed random number generator if not already seeded - if not utils._random_seeded then - math.randomseed(os.time()) - utils._random_seeded = true - end - - local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" - - local result = template:gsub("[xy]", function(c: string): string - local v = (c == "x") and math.random(0, 15) or math.random(8, 11) - return string.format("%x", v) - end) - return result -end - ----Generate random hex string of specified length ----@param length number Number of hex characters to generate ----@return string hex_string Random hex string -function utils.generate_hex(length: number): string - if not utils._random_seeded then - math.randomseed(os.time()) - utils._random_seeded = true - end - - local hex_chars = "0123456789abcdef" - local result: {string} = {} - - for _ = 1, length do - local idx = math.random(1, #hex_chars) - table.insert(result, hex_chars:sub(idx, idx)) - end - - return table.concat(result) -end - ----URL encode a string ----@param str string String to encode ----@return string encoded_string URL encoded string -function utils.url_encode(str: string): string - if not str then - return "" - end - - str = tostring(str) - - -- Replace unsafe characters with percent encoding - str = str:gsub("([^%w%-%.%_%~])", function(c: string): string - return string.format("%%%02X", string.byte(c)) - end) - - return str -end - ----URL decode a string ----@param str string String to decode ----@return string decoded_string URL decoded string -function utils.url_decode(str: string): string - if not str then - return "" - end - - str = tostring(str) - - -- Replace percent encoding with actual characters - str = str:gsub("%%(%x%x)", function(hex: string): string - return string.char(tonumber(hex, 16) as integer) - end) - - return str -end - ----Check if a string is empty or nil ----@param str string? String to check ----@return boolean is_empty True if string is nil or empty -function utils.is_empty(str: string): boolean - return not str or str == "" -end - ----Trim whitespace from both ends of a string ----@param str string String to trim ----@return string trimmed_string String with whitespace removed from both ends -function utils.trim(str: string): string - if not str then - return "" - end - - return str:match("^%s*(.-)%s*$") or "" -end - ----Deep copy a table ----@param orig table Original table ----@return table copy Deep copy of the table -function utils.deep_copy(orig: T): T - local copy: any - if type(orig) == "table" then - copy = {} - local orig_table = orig as {any: any} - for orig_key, orig_value in next, orig_table, nil do - (copy as {any: any})[utils.deep_copy(orig_key)] = utils.deep_copy(orig_value) - end - setmetatable(copy as table, utils.deep_copy(getmetatable(orig as table))) - else - copy = orig - end - return copy as T -end - ----Merge two tables (shallow merge) ----@param t1 table First table ----@param t2 table Second table ----@return table merged_table Merged table -function utils.merge_tables(t1: {K: V}, t2: {K: V}): {K: V} - local result: {K: V} = {} - - if t1 then - for k, v in pairs(t1) do - result[k] = v - end - end - - if t2 then - for k, v in pairs(t2) do - result[k] = v - end - end - - return result -end - ----Get current timestamp in seconds ----@return number timestamp Current timestamp -function utils.get_timestamp(): number - return os.time() -end - ----Get current timestamp in milliseconds (best effort) ----@return number timestamp Current timestamp in milliseconds -function utils.get_timestamp_ms(): number - -- Try to use socket.gettime if available for higher precision - local success, socket_module = pcall(require, "socket") - if success and socket_module and type(socket_module) == "table" then - local socket_table = socket_module as {string: any} - if socket_table["gettime"] and type(socket_table["gettime"]) == "function" then - local gettime = socket_table["gettime"] as function(): number - return math.floor(gettime() * 1000) - end - end - - -- Fallback to seconds * 1000 - return os.time() * 1000 -end - -return utils \ No newline at end of file diff --git a/src/sentry/utils/dsn.tl b/src/sentry/utils/dsn.tl deleted file mode 100644 index e5e70b0..0000000 --- a/src/sentry/utils/dsn.tl +++ /dev/null @@ -1,110 +0,0 @@ -local version = require("sentry.version") - -local record DSN - protocol: string - public_key: string - secret_key: string - host: string - port: number - path: string - project_id: string -end - -local function parse_dsn(dsn_string: string): DSN, string - if not dsn_string or dsn_string == "" then - return {} as DSN, "DSN is required" - end - - -- Pattern to match DSN with optional secret key - -- https://public_key:secret_key@host:port/path/project_id - -- https://public_key@host:port/path/project_id - local protocol, credentials, host_path = dsn_string:match("^(https?)://([^@]+)@(.+)$") - - if not protocol or not credentials or not host_path then - return {} as DSN, "Invalid DSN format" - end - - -- Parse credentials (public_key or public_key:secret_key) - local public_key, secret_key = credentials:match("^([^:]+):(.+)$") - if not public_key then - public_key = credentials - secret_key = "" - end - - if not public_key or public_key == "" then - return {} as DSN, "Invalid DSN format" - end - - -- Parse host and path - local host, path = host_path:match("^([^/]+)(.*)$") - if not host or not path or path == "" then - return {} as DSN, "Invalid DSN format" - end - - -- Extract project ID from path (last numeric segment) - local project_id = path:match("/([%d]+)$") - if not project_id then - return {} as DSN, "Could not extract project ID from DSN" - end - - -- Parse port from host - local port = 443 - if protocol == "http" then - port = 80 - end - - local host_part, port_part = host:match("^([^:]+):?(%d*)$") - if host_part then - host = host_part - if port_part and port_part ~= "" then - port = tonumber(port_part) or port - end - end - - return { - protocol = protocol, - public_key = public_key, - secret_key = secret_key or "", - host = host, - port = port, - path = path, - project_id = project_id - }, nil -- Return nil instead of empty string for no error -end - -local function build_ingest_url(dsn: DSN): string - return string.format("%s://%s/api/%s/store/", - dsn.protocol, - dsn.host, - dsn.project_id) -end - -local function build_envelope_url(dsn: DSN): string - return string.format("%s://%s/api/%s/envelope/", - dsn.protocol, - dsn.host, - dsn.project_id) -end - -local function build_auth_header(dsn: DSN): string - local auth_parts = { - "Sentry sentry_version=7", - "sentry_key=" .. dsn.public_key, - "sentry_client=sentry-lua/" .. version - } - - if dsn.secret_key and dsn.secret_key ~= "" then - table.insert(auth_parts, "sentry_secret=" .. dsn.secret_key) - end - - return table.concat(auth_parts, ", ") -end - - -return { - parse_dsn = parse_dsn, - build_ingest_url = build_ingest_url, - build_envelope_url = build_envelope_url, - build_auth_header = build_auth_header, - DSN = DSN -} \ No newline at end of file diff --git a/src/sentry/utils/envelope.tl b/src/sentry/utils/envelope.tl deleted file mode 100644 index 97abd18..0000000 --- a/src/sentry/utils/envelope.tl +++ /dev/null @@ -1,84 +0,0 @@ ----@class sentry.utils.envelope ---- Envelope formatting utilities for Sentry transactions, performance data, and logs - -local json = require("sentry.utils.json") - ----Build an envelope for a transaction ----@param transaction table Transaction data ----@param event_id string Event ID ----@return string envelope_body The formatted envelope body -local function build_transaction_envelope(transaction: table, event_id: string): string - -- Get current timestamp in RFC 3339 format - local sent_at = os.date("!%Y-%m-%dT%H:%M:%SZ") - - -- Envelope header - local envelope_header = { - event_id = event_id, - sent_at = sent_at - } - - -- Transaction JSON payload - local transaction_json = json.encode(transaction) - local payload_length = string.len(transaction_json) - - -- Item header - local item_header = { - type = "transaction", - length = payload_length - } - - -- Build envelope: header + newline + item header + newline + payload - local envelope_parts: {string} = { - json.encode(envelope_header), - json.encode(item_header), - transaction_json - } - - return table.concat(envelope_parts, "\n") -end - ----Build an envelope for log records ----@param log_records table Array of log records ----@return string envelope_body The formatted envelope body -local function build_log_envelope(log_records: {any}): string - if not log_records or #log_records == 0 then - return "" - end - - -- Get current timestamp in RFC 3339 format - local sent_at: string = os.date("!%Y-%m-%dT%H:%M:%SZ") or "" - - -- Envelope header - local envelope_header: {string: any} = { - sent_at = sent_at - } - - -- Build log items array (all logs in a single envelope item) - local log_items: {string: any} = { - items = log_records - } - - -- Log JSON payload - local log_json: string = json.encode(log_items) - - -- Item header for log envelope - local item_header: {string: any} = { - type = "log", - item_count = #log_records, - content_type = "application/vnd.sentry.items.log+json" - } - - -- Build envelope: header + newline + item header + newline + payload - local envelope_parts: {string} = { - json.encode(envelope_header), - json.encode(item_header), - log_json - } - - return table.concat(envelope_parts, "\n") -end - -return { - build_transaction_envelope = build_transaction_envelope, - build_log_envelope = build_log_envelope -} \ No newline at end of file diff --git a/src/sentry/utils/http.tl b/src/sentry/utils/http.tl deleted file mode 100644 index 150e6fb..0000000 --- a/src/sentry/utils/http.tl +++ /dev/null @@ -1,178 +0,0 @@ --- Platform-agnostic HTTP handling -local record HTTPResponse - status: number - body: string - success: boolean - error: string -end - -local record HTTPRequest - url: string - method: string - headers: {string: string} - body: string - timeout: number -end - --- Platform-specific HTTP implementations -local http_impl = nil -local http_type = "none" - --- Try different HTTP libraries -local function try_luasocket() - local success, http = pcall(require, "socket.http") - local success_https, https = pcall(require, "ssl.https") - local success_ltn12, ltn12 = pcall(require, "ltn12") - if success and success_ltn12 then - return { - request = function(req: HTTPRequest): HTTPResponse - local url = req.url - local is_https = url:match("^https://") - local http_lib = (is_https and success_https) and https or http - - if not http_lib then - return { - success = false, - error = "HTTPS not supported", - status = 0, - body = "" - } - end - - local response_body = {} - local result, status = http_lib.request({ - url = url, - method = req.method, - headers = req.headers, - source = req.body and ltn12.source.string(req.body) or nil, - sink = ltn12.sink.table(response_body) - }) - - return { - success = result ~= nil, - status = status or 0, - body = table.concat(response_body or {}), - error = result and "" or "HTTP request failed" - } - end - }, "luasocket" - end - return nil, nil -end - -local function try_roblox() - if _G.game and _G.game.GetService then - local HttpService = game:GetService("HttpService") - if HttpService then - return { - request = function(req: HTTPRequest): HTTPResponse - local success, response = pcall(function() - return HttpService:RequestAsync({ - Url = req.url, - Method = req.method, - Headers = req.headers, - Body = req.body - }) - end) - - if success and response then - return { - success = true, - status = response.StatusCode, - body = response.Body, - error = "" - } - else - return { - success = false, - status = 0, - body = "", - error = tostring(response) - } - end - end - }, "roblox" - end - end - return nil, nil -end - -local function try_openresty() - if _G.ngx then - local success, httpc = pcall(require, "resty.http") - if success then - return { - request = function(req: HTTPRequest): HTTPResponse - local http_client = httpc:new() - http_client:set_timeout((req.timeout or 30) * 1000) - - local res, err = http_client:request_uri(req.url, { - method = req.method, - body = req.body, - headers = req.headers - }) - - if res then - return { - success = true, - status = res.status, - body = res.body, - error = "" - } - else - return { - success = false, - status = 0, - body = "", - error = err or "HTTP request failed" - } - end - end - }, "openresty" - end - end - return nil, nil -end - --- Try implementations in order -local implementations = { - try_roblox, -- Try Roblox first (most restrictive) - try_openresty, -- Try OpenResty - try_luasocket, -- Try standard Lua -} - -for _, impl_func in ipairs(implementations) do - local impl, impl_type = impl_func() - if impl then - http_impl = impl - http_type = impl_type - break - end -end - --- Fallback implementation (no-op) -if not http_impl then - http_impl = { - request = function(req: HTTPRequest): HTTPResponse - return { - success = false, - status = 0, - body = "", - error = "No HTTP implementation available" - } - end - } - http_type = "none" -end - -local function request(req: HTTPRequest): HTTPResponse - return http_impl.request(req) -end - -return { - request = request, - available = http_type ~= "none", - type = http_type, - HTTPRequest = HTTPRequest, - HTTPResponse = HTTPResponse -} \ No newline at end of file diff --git a/src/sentry/utils/json.tl b/src/sentry/utils/json.tl deleted file mode 100644 index e4a663f..0000000 --- a/src/sentry/utils/json.tl +++ /dev/null @@ -1,109 +0,0 @@ --- Platform-agnostic JSON handling -local json_lib = {} - --- Try to load JSON library with fallbacks -local json_impl = nil -local json_type = "none" - --- Try different JSON libraries in order of preference -local json_libraries = { - {name = "cjson", type = "cjson"}, - {name = "dkjson", type = "dkjson"}, - {name = "json", type = "json"}, -} - -for _, lib in ipairs(json_libraries) do - local success, json_module = pcall(require, lib.name) - if success then - json_impl = json_module - json_type = lib.type - break - end -end - --- Roblox has built-in JSON -if not json_impl and _G.game and _G.game.GetService then - local HttpService = game:GetService("HttpService") - if HttpService then - json_impl = { - encode = function(obj) return HttpService:JSONEncode(obj) end, - decode = function(str) return HttpService:JSONDecode(str) end - } - json_type = "roblox" - end -end - --- Simple fallback JSON encoder (basic, but works) -if not json_impl then - local function simple_encode(obj) - if type(obj) == "string" then - return '"' .. obj:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"' - elseif type(obj) == "number" or type(obj) == "boolean" then - return tostring(obj) - elseif obj == nil then - return "null" - elseif type(obj) == "table" then - local result = {} - local is_array = true - local array_index = 1 - - -- Check if it's an array - for k, _ in pairs(obj) do - if k ~= array_index then - is_array = false - break - end - array_index = array_index + 1 - end - - if is_array then - -- Array - for i, v in ipairs(obj) do - table.insert(result, simple_encode(v)) - end - return "[" .. table.concat(result, ",") .. "]" - else - -- Object - for k, v in pairs(obj) do - if type(k) == "string" then - table.insert(result, '"' .. k .. '":' .. simple_encode(v)) - end - end - return "{" .. table.concat(result, ",") .. "}" - end - end - return "null" - end - - json_impl = { - encode = simple_encode, - decode = function(str) - error("JSON decoding not supported in fallback mode") - end - } - json_type = "fallback" -end - --- Unified interface -local function encode(obj) - if json_type == "dkjson" then - return json_impl.encode(obj) - else - return json_impl.encode(obj) - end -end - -local function decode(str) - if json_type == "dkjson" then - return json_impl.decode(str) - else - return json_impl.decode(str) - end -end - -return { - encode = encode, - decode = decode, - available = json_impl ~= nil, - type = json_type -} \ No newline at end of file diff --git a/src/sentry/utils/os.tl b/src/sentry/utils/os.tl deleted file mode 100644 index 9bfa0d8..0000000 --- a/src/sentry/utils/os.tl +++ /dev/null @@ -1,36 +0,0 @@ -local record OSInfo - name: string - version: string -end - --- Platform interface that each platform must implement -local record OSDetector - detect: function(): OSInfo -end - -local detectors: {OSDetector} = {} - -local function register_detector(detector: OSDetector) - table.insert(detectors, detector) -end - -local function get_os_info(): OSInfo - -- Try each registered detector until one succeeds - for _, detector in ipairs(detectors) do - local success, result = pcall(detector.detect) - if success and result then - return result - end - end - - -- If we can't detect anything, return nil - return nil -end - -return { - OSInfo = OSInfo, - OSDetector = OSDetector, - register_detector = register_detector, - get_os_info = get_os_info, - detectors = detectors -- Expose for testing -} \ No newline at end of file diff --git a/src/sentry/utils/runtime.tl b/src/sentry/utils/runtime.tl deleted file mode 100644 index 0053dcd..0000000 --- a/src/sentry/utils/runtime.tl +++ /dev/null @@ -1,122 +0,0 @@ --- Runtime detection utility for different Lua environments - -local record RuntimeInfo - name: string - version: string - description: string -end - -local function detect_standard_lua(): RuntimeInfo - -- Standard Lua runtime - local version = _VERSION or "Lua (unknown version)" - return { - name = "Lua", - version = version:match("Lua (%d+%.%d+)") or version, - description = version - } -end - -local function detect_luajit(): RuntimeInfo - -- LuaJIT detection - if jit and jit.version then - return { - name = "LuaJIT", - version = jit.version:match("LuaJIT (%S+)") or jit.version, - description = jit.version - } - end - return nil -end - -local function detect_roblox(): RuntimeInfo - -- Roblox Luau detection - if game and game.GetService then - -- Roblox environment detected - local version = "Unknown" - -- Try to get version info if available - if version and version ~= "" then - return { - name = "Luau", - version = version, - description = "Roblox Luau " .. version - } - else - return { - name = "Luau", - description = "Roblox Luau" - } - end - end - return nil -end - -local function detect_defold(): RuntimeInfo - -- Defold detection - if sys and sys.get_engine_info then - local engine_info = sys.get_engine_info() - return { - name = "defold", - version = engine_info.version or "unknown", - description = "Defold " .. (engine_info.version or "unknown") - } - end - return nil -end - -local function detect_love2d(): RuntimeInfo - -- Love2D detection - if love and love.getVersion then - local major, minor, revision, codename = love.getVersion() - local version = string.format("%d.%d.%d", major, minor, revision) - return { - name = "love2d", - version = version, - description = "LÖVE " .. version .. " (" .. (codename or "") .. ")" - } - end - return nil -end - -local function detect_openresty(): RuntimeInfo - -- OpenResty/ngx_lua detection - if ngx and ngx.var then - local version = "unknown" - if ngx.config and ngx.config.ngx_lua_version then - version = ngx.config.ngx_lua_version - end - return { - name = "OpenResty", - version = version, - description = "OpenResty/ngx_lua " .. version - } - end - return nil -end - -local function get_runtime_info(): RuntimeInfo - -- Try to detect specific environments first - local detectors = { - detect_roblox, - detect_defold, - detect_love2d, - detect_openresty, - detect_luajit - } - - for _, detector in ipairs(detectors) do - local result = detector() - if result then - return result - end - end - - -- Fall back to standard Lua - return detect_standard_lua() -end - -local runtime = { - get_runtime_info = get_runtime_info, - RuntimeInfo = RuntimeInfo -} - -return runtime \ No newline at end of file diff --git a/src/sentry/utils/serialize.tl b/src/sentry/utils/serialize.tl deleted file mode 100644 index 6767dc8..0000000 --- a/src/sentry/utils/serialize.tl +++ /dev/null @@ -1,76 +0,0 @@ -local json = require("sentry.utils.json") -local version = require("sentry.version") - -local record EventData - event_id: string - timestamp: number - level: string - logger: string - platform: string - sdk: table - message: string - exception: table - stacktrace: table - user: table - tags: {string: string} - extra: {string: any} - breadcrumbs: {table} - environment: string - release: string - contexts: {string: any} -end - -local function generate_event_id(): string - local chars = "abcdef0123456789" - local result: {string} = {} - - for _ = 1, 32 do - local rand_idx = math.random(1, #chars) - table.insert(result, chars:sub(rand_idx, rand_idx)) - end - - return table.concat(result) -end - --- Create a basic event structure - scope data will be applied separately -local function create_event(level: string, message: string, environment: string, release: string, stack_trace: table): EventData - local event: EventData = { - event_id = generate_event_id(), - timestamp = os.time(), - level = level, - platform = "lua", - sdk = { - name = "sentry.lua", - version = version - }, - message = message, - environment = environment or "production", - release = release, - user = {}, - tags = {}, - extra = {}, - breadcrumbs = {}, - contexts = {} - } - - if stack_trace and stack_trace.frames then - event.stacktrace = { - frames = stack_trace.frames - } - end - - return event -end - -local function serialize_event(event: EventData): string - return json.encode(event) -end - -local serialize = { - create_event = create_event, - serialize_event = serialize_event, - generate_event_id = generate_event_id, - EventData = EventData -} - -return serialize \ No newline at end of file diff --git a/src/sentry/utils/stacktrace.tl b/src/sentry/utils/stacktrace.tl deleted file mode 100644 index 0b65f8c..0000000 --- a/src/sentry/utils/stacktrace.tl +++ /dev/null @@ -1,204 +0,0 @@ -local record StackFrame - filename: string - ["function"]: string -- Sentry expects "function", not "function_name" - lineno: number - in_app: boolean - vars: {string:any} -- local variables and parameters - abs_path: string - context_line: string - pre_context: {string} - post_context: {string} -end - -local record StackTrace - frames: {StackFrame} -end - --- Read source context from a file -local function get_source_context(filename: string, line_number: number): string, {string}, {string} - local empty_array: {string} = {} - - if line_number <= 0 then - return "", empty_array, empty_array - end - - -- Try to open and read the file - local file = io.open(filename, "r") - if not file then - return "", empty_array, empty_array - end - - -- Read all lines into array (using dynamic indexing to avoid Teal issues) - local all_lines: {any} = {} - local line_count = 0 - for line in file:lines() do - line_count = line_count + 1 - all_lines[line_count] = line - end - file:close() - - -- Extract context lines - local context_line = "" - local pre_context: {string} = {} - local post_context: {string} = {} - - if line_number > 0 and line_number <= line_count then - context_line = (all_lines[line_number] as string) or "" - - -- Get 5 lines before - for i = math.max(1, line_number - 5), line_number - 1 do - if i >= 1 and i <= line_count then - table.insert(pre_context, (all_lines[i] as string) or "") - end - end - - -- Get 5 lines after - for i = line_number + 1, math.min(line_count, line_number + 5) do - if i >= 1 and i <= line_count then - table.insert(post_context, (all_lines[i] as string) or "") - end - end - end - - return context_line, pre_context, post_context -end - -local function get_stack_trace(skip_frames: number): StackTrace - skip_frames = skip_frames or 0 - local frames: {StackFrame} = {} - local level = 2 + (skip_frames or 0) - - while true do - local info = debug.getinfo(level as integer, "nSluf") - if not info then - break - end - - local filename = info.source or "unknown" - if filename:sub(1, 1) == "@" then - filename = filename:sub(2) - elseif filename == "=[C]" then - filename = "[C]" - end - - -- Determine if this frame is part of the user's application - local in_app = true - if not info.source then - in_app = false -- No source info - elseif filename == "[C]" then - in_app = false -- C function - elseif info.source:match("sentry") then - in_app = false -- SDK code - elseif filename:match("^/opt/homebrew") then - in_app = false -- Homebrew-installed libraries - end - - -- Get function name with better fallback - local function_name = info.name or "anonymous" - if info.namewhat and info.namewhat ~= "" then - function_name = info.name or "anonymous" - elseif info.what == "main" then - function_name = "
" - elseif info.what == "C" then - function_name = info.name or "" - end - - -- Collect local variables and parameters - local vars: {string:any} = {} - if info.what == "Lua" and in_app then -- Only collect vars for user code - -- Get parameters first (they're usually the first N locals) - for i = 1, (info.nparams or 0) do - local name, value = debug.getlocal(level as integer, i) - if name and not name:match("^%(") then -- Skip internal vars like (*temporary) - local safe_value = value - local value_type = type(value) - if value_type == "function" then - safe_value = "" - elseif value_type == "userdata" then - safe_value = "" - elseif value_type == "thread" then - safe_value = "" - elseif value_type == "table" then - safe_value = "" - end - vars[name] = safe_value - end - end - - -- Get other local variables (skip parameters we already got) - for i = (info.nparams or 0) + 1, 20 do -- reasonable limit - local name, value = debug.getlocal(level as integer, i) - if not name then break end - if not name:match("^%(") then -- Skip internal vars like (*temporary) - local safe_value = value - local value_type = type(value) - if value_type == "function" then - safe_value = "" - elseif value_type == "userdata" then - safe_value = "" - elseif value_type == "thread" then - safe_value = "" - elseif value_type == "table" then - safe_value = "
" - end - vars[name] = safe_value - end - end - end - - -- Ensure line number is non-negative for Sentry compatibility - local line_number = info.currentline or 0 - if line_number < 0 then - line_number = 0 - end - - -- Get source context if this is an in-app frame with valid line number - local context_line, pre_context, post_context = get_source_context(filename, line_number) - - local frame: StackFrame = { - filename = filename, - ["function"] = function_name, - lineno = line_number, - in_app = in_app, - vars = vars, - abs_path = filename, -- For now, same as filename - context_line = context_line, - pre_context = pre_context, - post_context = post_context - } - - table.insert(frames, frame) - level = level + 1 - end - - -- Invert frames for Sentry (outermost to innermost) - local inverted_frames: {StackFrame} = {} - for i = #frames, 1, -1 do - table.insert(inverted_frames, frames[i]) - end - - return {frames = inverted_frames} -end - -local function format_stack_trace(stack_trace: StackTrace): string - local lines: {string} = {} - - for _, frame in ipairs(stack_trace.frames) do - local line = string.format(" %s:%d in %s", - frame.filename, - frame.lineno as integer, - frame["function"]) - table.insert(lines, line) - end - - return table.concat(lines, "\n") -end - -local stacktrace = { - get_stack_trace = get_stack_trace, - format_stack_trace = format_stack_trace, - StackTrace = StackTrace, - StackFrame = StackFrame -} - -return stacktrace \ No newline at end of file diff --git a/src/sentry/utils/transport.tl b/src/sentry/utils/transport.tl deleted file mode 100644 index 96ce5b7..0000000 --- a/src/sentry/utils/transport.tl +++ /dev/null @@ -1,56 +0,0 @@ --- Platform-agnostic transport registry with dependency injection -local record Transport - send: function(self: Transport, event: table): boolean, string - configure: function(self: Transport, config: table): Transport -end - -local record TransportFactory - name: string - priority: number - create: function(config: table): Transport - is_available: function(): boolean -end - -local factories: {TransportFactory} = {} - --- Register a transport factory -local function register_transport_factory(factory: TransportFactory) - table.insert(factories, factory) - -- Sort by priority (higher priority first) - table.sort(factories, function(a: TransportFactory, b: TransportFactory): boolean - return a.priority > b.priority - end) -end - --- Get the best available transport for the current platform -local function create_transport(config: table): Transport - for _, factory in ipairs(factories) do - if factory.is_available() then - return factory.create(config) - end - end - - -- Fallback to test transport if nothing else works - local TestTransport = require("sentry.core.test_transport") - return TestTransport:configure(config) as Transport -end - --- Get available transport names for debugging -local function get_available_transports(): {string} - local available = {} - for _, factory in ipairs(factories) do - if factory.is_available() then - table.insert(available, factory.name) - end - end - return available -end - -return { - Transport = Transport, - TransportFactory = TransportFactory, - register_transport_factory = register_transport_factory, - create_transport = create_transport, - get_available_transports = get_available_transports, - factories = factories -} \ No newline at end of file diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..f043113 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,14 @@ +# StyLua configuration file + +column_width = 200 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "Always" + +# Collapse simple structures to single line if they fit +collapse_simple_statement = "Always" + +[sort_requires] +enabled = false \ No newline at end of file diff --git a/tealdoc.yml b/tealdoc.yml deleted file mode 100644 index c78367b..0000000 --- a/tealdoc.yml +++ /dev/null @@ -1,41 +0,0 @@ -project_name: "Sentry Lua SDK" -project_version: "0.0.6" -source_dir: "src" -output_dir: "docs" -title: "Sentry SDK for Lua Documentation" -description: "Platform-agnostic Sentry SDK for Lua environments" - -modules: - - name: "sentry" - path: "src/sentry/init.tl" - description: "Main Sentry SDK interface" - - - name: "sentry.core.client" - path: "src/sentry/core/client.tl" - description: "Core client implementation" - - - name: "sentry.core.transport" - path: "src/sentry/core/transport.tl" - description: "Event transport abstraction" - - - name: "sentry.core.context" - path: "src/sentry/core/context.tl" - description: "Context and breadcrumb management" - - - name: "sentry.utils.stacktrace" - path: "src/sentry/utils/stacktrace.tl" - description: "Stack trace utilities" - - - name: "sentry.utils.serialize" - path: "src/sentry/utils/serialize.tl" - description: "Event serialization utilities" - -examples: - - name: "Basic Usage" - file: "examples/basic.lua" - - - name: "Redis Integration" - file: "examples/redis.lua" - - - name: "nginx Integration" - file: "examples/nginx.lua" \ No newline at end of file diff --git a/tlconfig.lua b/tlconfig.lua deleted file mode 100644 index 521a232..0000000 --- a/tlconfig.lua +++ /dev/null @@ -1,14 +0,0 @@ -return { - source_dir = "src", - build_dir = "build", - module_name = "sentry", - include_dir = { - "src", - }, - external_libs = { - "busted", - }, - target_extension = "lua", - gen_compat = "optional", - gen_target = "5.1" -} \ No newline at end of file