Replace JSON.NET with System.Text.Json across the codebase#2135
Open
niemyjski wants to merge 3481 commits into
Open
Replace JSON.NET with System.Text.Json across the codebase#2135niemyjski wants to merge 3481 commits into
niemyjski wants to merge 3481 commits into
Conversation
Introduces a new skill for automatically generating user-friendly changelogs from git commit history. This skill analyzes commits, categorizes changes, translates technical jargon into customer-friendly language, and formats the output professionally. It helps streamline the release note creation process, saving time and ensuring clear communication with users.
Removes the specific build step for the Elasticsearch 7 all-in-one Docker image during release builds. This simplifies the build process by focusing on core images.
…lar (#2066) Bumps [qs](https://github.com/ljharb/qs) from 6.5.3 to 6.14.1. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](ljharb/qs@v6.5.3...v6.14.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.14.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Upgraded to xunit.v3 * Updated to latest foundatio nightlies * Cleans up event user description queue Ensures the event user description queue is properly cleaned up during tests to prevent potential interference between test runs. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Updates various package dependencies to their latest stable versions. This ensures compatibility, incorporates new features, and addresses potential security vulnerabilities.
Adds a code coverage configuration file to specify which modules should be included or excluded from coverage analysis. Updates the build workflow to use the new configuration file when collecting code coverage data during .NET tests. This allows for more granular control over coverage reporting.
Introduces dedicated tests for each model to ensure correct serialization and deserialization. This change replaces the previous `ModelTests` with granular tests, improving test coverage and simplifying debugging related to model serialization. Each model now has its own test class to validate its specific serialization and deserialization behavior.
Adds integration tests for the Organization, Project, and Token controllers. These tests cover mapping logic and API endpoint behavior, ensuring that data transformations and controller actions function as expected. This includes validating the creation, retrieval, and updates of entities, as well as verifying validation rules and permission checks.
Adds tests to ensure posting events with extra, complex, or mixed-case properties correctly captures the data in the data bag or known properties. These tests verify that extra root properties are captured within the event's data bag, known data keys are properly deserialized, complex properties are handled as objects, and different naming conventions (snake_case, PascalCase) are correctly processed.
Ensures correct deserialization of events by updating the expected JSON structure and filtering out session start events when retrieving events. The "@Version" field is used instead of "version" and session start events are ignored.
Adds new agent skills to enhance the system's capabilities, including: - Agent Browser: Automates browser interactions - Frontend Design: Creates high-quality frontend interfaces - Release Notes: Generates formatted changelogs from Git history - Stripe Best Practices: Provides guidance for Stripe integrations - Upgrade Stripe: Offers a guide for upgrading Stripe API versions and SDKs Also, adds and updates skills related to backend testing and .NET conventions, improving code quality and testing processes. Includes a script to automate the addition of skills.
Updates OpenTelemetry packages in both the Exceptionless.Job and Exceptionless.Web projects to version 1.15.0. This brings in the latest features, bug fixes, and performance improvements from the OpenTelemetry library.
…th STJ (#2041) * Replace SwashBuckle with Scalar, Replace JSON.NET from API Surface with STJ * Removes duplicate package references Removes duplicate package references and cleans up unused package references in the Exceptionless.Web project file. This ensures that the project has a consistent and minimal set of dependencies. * Removed json.net * Removes Delta operation filter. Removes the DeltaOperationFilter as it is no longer needed. * Updates Scalar.AspNetCore package Updates the Scalar.AspNetCore package to the latest version. This change also updates the Scalar configuration to support multiple API documentation versions and includes updates to the route pattern and authentication. * Improves docs and authentication setup Updates documentation configuration to streamline access and improve security. Redirects the `/docs/index.html` path to `/docs` for a cleaner user experience. Simplifies the API key authentication scheme in the documentation, replacing username/password and token authentication. The key is renamed to "Token" and the security scheme is updated to "Bearer". This change provides a more straightforward and modern approach to API authentication documentation. * update deps * Uses System.Text.Json for WebSocket serialization. Migrates WebSocket message serialization to use System.Text.Json with a lower-case underscore naming policy. This change ensures consistent serialization behavior across the application, improving maintainability and reducing potential issues related to different JSON serialization libraries. The naming policy enforces a standard for property names in serialized JSON, enhancing data exchange between client and server. * Configures STJ for tests Configures System.Text.Json serializer options in the test suite to use the custom naming policy and delta converter. This ensures consistent serialization behavior in tests. * reverted changes * Fixed url * updated node * Removes required keyword from auth models Updates authentication models to remove the `required` keyword. This change ensures that null values can be handled correctly, aligning with the desired behavior when using System.Text.Json. dotnet/runtime#110733 (comment) dotnet/aspnetcore#61511 (comment) * Moves to System.Text.Json serializer Converts the project to use System.Text.Json for serialization, replacing Newtonsoft.Json where possible. This change aims to improve performance and reduce dependencies by leveraging the built-in System.Text.Json library. Configures Newtonsoft.Json for some legacy usages. * Configures JsonSerializer to ignore null values Configures the JsonSerializerOptions to ignore null values when writing JSON. This prevents unnecessary null values from being included in the serialized output, resulting in more concise and efficient data transfer. * Adds support for inferred types during deserialization Adds a converter to handle deserialization of object properties to inferred types, enabling correct interpretation of different value types. This change also ensures that the TypeDescriptor converter is correctly retrieved before checking if it can perform the conversion. * Enhances DataDictionary deserialization Improves the `DataDictionary` extension methods to handle various JSON types, including `JsonDocument`, `JsonElement`, `JsonNode`, dictionaries, and lists, using System.Text.Json. This change ensures proper deserialization and type conversion of values stored in the dictionary, especially when data originates from different sources or serialization methods. It also updates event processing and formatting plugins to utilize JsonSerializerOptions for data retrieval. Addresses Elasticsearch compatibility by handling JObjects. Adds a new `ObjectToInferredTypesConverter` to deserialize object properties into appropriate .NET types. * Adds JsonSerializerOptions to EventExtensions This commit refactors the EventExtensions class to accept JsonSerializerOptions. Previously, certain methods in EventExtensions that retrieved data from an Event's Data property lacked a JsonSerializerOptions parameter, potentially leading to issues with deserialization when custom options were required. Now, these methods accept a JsonSerializerOptions parameter, and a try-catch block has been added to handle potential deserialization exceptions. This change ensures that the correct JsonSerializerOptions are used when deserializing data from the event and enhances the robustness of the code by gracefully handling deserialization errors. * Enhances OpenAPI schema generation Improves OpenAPI schema generation by: - Applying schema transformers to handle Delta types, read-only properties, unique items, and enum names correctly. - Removing Problem JSON content type from successful responses. - Adding support for request body content attributes. - Utilizing XML documentation for operation descriptions. These changes ensure accurate and comprehensive API documentation. * Optimizes number deserialization in JSON converter Improves number deserialization within the custom JSON converter by prioritizing smaller integer types (int) and decimal before falling back to larger types (long, double). This aims to improve boxing efficiency and precision when handling numeric values. It also adds DateTime support. * Updates Foundatio and Scalar dependencies Updates Foundatio packages to beta1.7. Updates Scalar.AspNetCore package to 2.12.13. * Migrates to System.Text.Json Updates formatting plugins to utilize System.Text.Json for event data serialization and deserialization. This change ensures consistency across the application by using a single JSON library. * Removes ITextSerializer contract tests Removes the ITextSerializer contract tests as these are no longer needed. The purpose of these tests were to ensure that all serialization methods work correctly and consistently, and that the serializer can be swapped without breaking functionality. Since the system text json serializer does not implement Foundatio.Serializer.ITextSerializer and the new code directly uses System.Text.Json this abstraction is no longer useful. * Ensures correct type inference in serializer This commit updates the serializer to correctly infer types, specifically changing integer type inference from `long` to `int` and ensuring decimal types are inferred as `decimal`. It also replaces `Assert.Contains` with `Assert.Equal` for JSON serialization tests, ensuring exact matching and improves test readability with language injection. * Improves JSON deserialization accuracy. Updates tests and JSON deserialization logic to correctly handle decimal values. It ensures that decimal types and values are properly preserved during deserialization, preventing potential data loss or incorrect interpretations. * Updates Scalar.AspNetCore package Updates the Scalar.AspNetCore package version to the latest release. This ensures compatibility and incorporates the latest features and bug fixes. * Adds authentication descriptions to OpenAPI Adds descriptions for Basic, Bearer, and Token authentication schemes to the OpenAPI documentation. This provides users with more information on how to authenticate with the API. * Fixed Scalar Auth * Adds continuous improvement guidelines Adds guidelines for continuous improvement to AGENTS.md, emphasizing the importance of updating documentation and skills after completing tasks or learning new information. The guidelines instruct agents to document project-specific knowledge and reusable domain patterns, while also restricting updates to third-party maintained skills. * Removes string type from numeric schemas .NET's OpenAPI generator adds ["number", "string"] for JavaScript BigInt compatibility. This change adds a schema transformer that removes the "string" type from numeric schemas, which prefers simple numeric types for TypeScript client generation. This ensures accurate client generation and avoids potential type-related issues. * Configures JSON serialization defaults Centralizes JSON serialization options configuration for consistency across the application. Applies snake_case naming, null value handling, dynamic object support, and enforces C# nullable annotations. * Adds validation to organization name Ensures that the organization name is required when creating a new organization. This prevents the creation of organizations without a name. * Handles nullable properties in Delta schema Ensures that nullable properties within Delta types are correctly represented in the OpenAPI schema. Adds logic to detect and handle nullable reference types and value types. This allows the API documentation to accurately reflect the possibility of null values for these properties. * Updates status code for validation errors. Changes the expected status code for validation errors from BadRequest (400) to UnprocessableEntity (422). This provides a more accurate representation of the error type when an entity fails validation. * Allows API generation from local file Enhances the API generation script to support reading the Swagger/OpenAPI definition from a local file path. This is useful for development workflows where the API definition is stored locally and needs to be regenerated without deploying. It also adds a check to ensure the file exists before attempting to generate the API. * Add data annotations for validation and ObjectId attribute in models * Add OpenAPI schema transformers for enhanced documentation and compatibility - Implement AggregateDocumentTransformer to manage IAggregate schema and fix CountResult.aggregations. - Introduce DataAnnotationHelper to apply DataAnnotation attributes to OpenAPI schemas. - Create DataAnnotationsSchemaTransformer to handle [EmailAddress], [Url], and [ObjectId] attributes. - Enhance DeltaSchemaTransformer to apply data annotations from inner properties. - Add DictionarySubclassSchemaTransformer to correctly handle dictionary subclass properties. - Implement ReadOnlyPropertySchemaTransformer to mark get-only properties as read-only and adjust nullability. - Create RequiredPropertySchemaTransformer to mark non-nullable properties as required in OpenAPI schemas. - Add SchemaReferenceIdHelper for consistent naming of generic types in OpenAPI schemas. - Update XEnumNamesSchemaTransformer to include enum values and x-enumNames extension for better TypeScript generation. * Add data annotations for validation and ObjectId attributes in multiple models * Updates for OpenAPI 3.1 * Added obsolete attribute support * Addressed code quality and build linting errors * Apply suggestion from @niemyjski * PR Feedback * Apply suggestion from @niemyjski * Potential fix for pull request finding 'Nested 'if' statements can be combined' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * PR Feedback * restored trx reporting * Upgraded sonar --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
System.Text.Json cannot deserialize into getter-only collection properties
(e.g., `{ get; } = new()`). When objects are serialized to cache and later
deserialized, STJ creates new empty collections instead of populating the
existing ones, causing data loss.
This was exposed after switching from Newtonsoft.Json to STJ for cache
serialization. The issue was most visible with User.OrganizationIds returning
empty, which caused impersonation detection to fail.
Changed from `{ get; }` to `{ get; init; }`:
- User.OrganizationIds
- User.OAuthAccounts
- OAuthAccount.ExtraData
- WorkInProgressResult.Workers
- ModelActionResults.Success
- ModelActionResults.Failure
- Created snapshot-refs.md for compact element references and usage guidelines. - Added video-recording.md for capturing browser automation as video. - Introduced authenticated-session.sh template for managing login sessions. - Added capture-workflow.sh template for extracting content from web pages. - Created form-automation.sh template for filling and submitting web forms. - Added LICENSE.txt for frontend-design skill with Apache License 2.0. - Introduced SKILL.md for frontend-design to guide distinctive UI creation. - Added SKILL.md for releasenotes to generate formatted changelogs from git history. - Created SKILL.md for stripe-best-practices outlining integration recommendations. - Added SKILL.md for upgrade-stripe detailing API versioning and upgrade processes.
…lization Changed constructor parameter from 'enableArchive' to 'shouldArchive' to match the 'ShouldArchive' property name. System.Text.Json requires constructor parameters to match property names (case-insensitive) for proper deserialization. This resolves the deserialization error when dequeuing EventPost from Redis.
…dev (#2105) * Migrates to Aspire Azure Storage Emulator instead of MinIO for local dev Replaces MinIO with Azure Storage for blob and queue storage. This change aligns with the move to Azure services and simplifies the infrastructure setup. It also updates the configuration to use Azure Storage connection strings. * removed minio * Excludes .agents directory from linting Updates eslint and prettier ignore files to exclude the .agents directory. This prevents linting and formatting of generated agent files, which are third-party and should not be modified. This change supports the aspire-azure-storage feature branch. * Fixed build errors.
- ObjectToInferredTypesConverter: Handle exponent numbers (e.g. 1e100) that overflow decimal range by falling back to double via TryGetDecimal - DataDictionaryExtensions: Map both SnakeCaseLower(PropertyName) and JsonPropertyName attribute forms in GetPropertyMap, and normalize keys to the attribute name (e.g. 'o_s_name' not 'os_name') in NormalizeKeysForType - V1 webhook fixtures: Remove null/empty fields to match STJ global options (WhenWritingNull + SkipEmptyCollections) — standard JSON convention, semantically equivalent for all webhook consumers
* Polish Svelte event and settings UX Improve event detail tabs, session navigation, settings layouts, table actions, scrollbar stability, and generated sample extended data. * Fix Svelte UI formatting
* Add project sample data generation * Address sample generation review feedback * Format sample data configure action * Address sample data PR feedback * Format sample data configure button
* Polish saved views and filters * Fix faceted filter formatting * Fix client lint issues * Update OpenAPI saved view sort baseline * Address saved view PR feedback
* feat: Add stacks dashboard page route and API query - Add getStacksQuery function for listing stacks with filtering - Create /stacks route with basic stacks list page - Support filtering by status, tags, date ranges, and custom Lucene expressions - Display stack information: title, tags, event count, first/last occurrence, status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * build: Simplify stacks page to avoid table library issues - Remove complex TanStack table setup that was causing build errors - Use semantic HTML table with Tailwind styling for simplicity - Implement row selection with checkboxes and bulk actions UI - Support filter input, limit selector, and pagination controls - Display stack properties: title, tags, event count, last occurrence, status - Add loading and empty state handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add stacks bulk actions button with all operations - Create StacksBulkActionsButton component for simplified bulk operations - Support mark open/fixed/snoozed/ignored/discarded and delete actions - Integrate with RemoveStackDialog and status change dialogs - Wire bulk actions into stacks page with selection tracking - All dialogs and confirmations included Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add stacks dashboard to navigation menu - Add stacks navigation item to Dashboards group - Use layers icon for visual consistency - Place between issues and event stream - Accessible from sidebar navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: Add stacks filter and bulk operation examples to HTTP tests - Add filter query examples (status:discarded, tags:production, type:error) - Add bulk operation examples (change status, mark fixed, mark snoozed, delete) - Document filtering syntax for stacks dashboard feature - Help users understand query capabilities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: rewrite stacks dashboard using proper patterns - Rewrote stacks page using useFetchClient, createTable, getSharedTableOptions, and DataTable components matching the issues/sessions page patterns - Created proper table column definitions with NumberFormatter, TimeAgo, and badge components for status/tags - Created StacksDataTable component mirroring EventsDataTable - Uses existing TableStacksBulkActionsDropdownMenu (deleted the duplicate stacks-bulk-actions-button.svelte) - Moved navigation from Dashboards to Reports group - Removed broken getStacksQuery and list query key from api.svelte.ts - Uses FacetedFilter system for filtering (status, project, date, etc.) - WebSocket StackChanged event handling for real-time updates - Offset-based pagination with page size selector - Fixed organization abbreviation (never use org) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: lint errors in stacks page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: clear stale organization selection state - Clear persisted organization id when no organizations remain - Re-select first valid organization when persisted id is stale - Hide manage/billing links when current organization is invalid - Prevent organization settings layout from redirecting to undefined ids Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve impersonation and stabilize stack refresh handling - Keep impersonated organization selections from being overwritten by org sync - Refresh stacks table on non-removed StackChanged events with throttling - Correct stacks HTTP samples to match ids path-segment routes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add stacks dashboard page route and API query - Add getStacksQuery function for listing stacks with filtering - Create /stacks route with basic stacks list page - Support filtering by status, tags, date ranges, and custom Lucene expressions - Display stack information: title, tags, event count, first/last occurrence, status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * build: Simplify stacks page to avoid table library issues - Remove complex TanStack table setup that was causing build errors - Use semantic HTML table with Tailwind styling for simplicity - Implement row selection with checkboxes and bulk actions UI - Support filter input, limit selector, and pagination controls - Display stack properties: title, tags, event count, last occurrence, status - Add loading and empty state handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add stacks bulk actions button with all operations - Create StacksBulkActionsButton component for simplified bulk operations - Support mark open/fixed/snoozed/ignored/discarded and delete actions - Integrate with RemoveStackDialog and status change dialogs - Wire bulk actions into stacks page with selection tracking - All dialogs and confirmations included Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Add stacks dashboard to navigation menu - Add stacks navigation item to Dashboards group - Use layers icon for visual consistency - Place between issues and event stream - Accessible from sidebar navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: Add stacks filter and bulk operation examples to HTTP tests - Add filter query examples (status:discarded, tags:production, type:error) - Add bulk operation examples (change status, mark fixed, mark snoozed, delete) - Document filtering syntax for stacks dashboard feature - Help users understand query capabilities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: rewrite stacks dashboard using proper patterns - Rewrote stacks page using useFetchClient, createTable, getSharedTableOptions, and DataTable components matching the issues/sessions page patterns - Created proper table column definitions with NumberFormatter, TimeAgo, and badge components for status/tags - Created StacksDataTable component mirroring EventsDataTable - Uses existing TableStacksBulkActionsDropdownMenu (deleted the duplicate stacks-bulk-actions-button.svelte) - Moved navigation from Dashboards to Reports group - Removed broken getStacksQuery and list query key from api.svelte.ts - Uses FacetedFilter system for filtering (status, project, date, etc.) - WebSocket StackChanged event handling for real-time updates - Offset-based pagination with page size selector - Fixed organization abbreviation (never use org) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: lint errors in stacks page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: clear stale organization selection state - Clear persisted organization id when no organizations remain - Re-select first valid organization when persisted id is stale - Hide manage/billing links when current organization is invalid - Prevent organization settings layout from redirecting to undefined ids Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve impersonation and stabilize stack refresh handling - Keep impersonated organization selections from being overwritten by org sync - Refresh stacks table on non-removed StackChanged events with throttling - Correct stacks HTTP samples to match ids path-segment routes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: align issue management and settings navigation UX Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: polish row interactions and issue detail links Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: clean up settings sidebar context Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: nest project settings under projects nav Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: show project submenu hierarchy in settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: render project settings as nested sidebar tree Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add settings separator after organizations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Polish sidebar hierarchy and issue management framing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move sessions into dashboards navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add collapsed sidebar hover flyouts for child nav Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert out-of-scope changes per PR review Reverted: Startup.cs (next build path), SavedView model/tests, organization-features extraction, events-overview pager, saved-view 422 error handling hack, issues page filter changes, project manage page link change. * Remove stacks page and navigation link per PR review The stacks dashboard was not ready for inclusion. Removed the route and its sidebar link from the layout. * Improve stacks table: rename severity to critical, fix tags cell UX - Renamed stack-severity-cell to stack-critical-cell (matches column name) - Added tooltip with Kbd for platform-agnostic shortcut hints on tags - Made overflow tags clickable in tooltip dropdown - Added getProjectStacksQuery tanstack wrapper in stacks API - Moved defaultColumnVisibility and getTableOptions to options file * Refactor project issues page to use TanStack Query Replaced manual useFetchClient calls with getProjectStacksQuery and getTableOptions from the options file. Removed unused showStackPager and onEventChange props from stack/event detail pages. * Cosmetic fixes: remove double newlines, simplify margin, extract \ - Removed extra blank lines in events and stream pages - Simplified account layout margin from mx-6 my-6 to m-6 - Extracted detailsHref into a \ in event-detail-sheet * Apply code formatting (npm run format) * Revert stacks view type additions from saved view models The stacks page was removed per review. These model changes adding 'stacks' as a valid ViewType are no longer needed. * Fix data-table click handling and stacks.http filter syntax - Don't intercept clicks on links/buttons in table cells (was breaking link navigation when rowHref not set) - Added keyboard accessibility (Enter/Space) to table cells - Fixed stacks.http filter: tags: → tag: (correct filter term) * Make org settings routes consistent, fix stacks viewType doc - Settings group now has all the same entries as Organization Settings (General, Usage, Billing, Features) for navigation consistency - Removed stale 'stacks' from SavedViewController viewType doc comment to match the reverted model validation * Revert tag filter from AND back to OR semantics Multi-tag filter should use OR (show events with any selected tag) not AND (require all tags). AND was an accidental breaking change. * Fix data-table keyboard a11y and stacks.http date format - Move tabindex from individual cells to row level (reduces tab stops) - Remove per-cell keydown handler, use row-level Enter/Space instead - Fix snoozeUntilUtc to use ISO-8601 format (2030-12-31T00:00:00Z) * Fix CI: prettier formatting and remove obsolete feature-gate tests - Fix prettier formatting in data-table-body.svelte (multiline onkeydown) - Remove 3 WhenFeatureDisabled tests since feature gate was removed from SavedViewController (saved views are now available to all orgs) * Fix ESLint curly rule: wrap if body in braces * Refactor feature flag documentation: remove specific feature references and clean up comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…son-v2 # Conflicts: # .gitignore # src/Exceptionless.Web/Controllers/ProjectController.cs
RCA: Merging origin/main into this branch left stale package entries from the pre-STJ-migration era alongside their new replacements: - Core.csproj: Foundatio.Repositories.Elasticsearch 7.18.3 alongside 8.0.0-beta1.3 (same Condition, duplicate PackageReference NU1504 error) - Web.csproj: NEST.JsonNetSerializer (no longer used — entire JSON.NET stack removed), duplicate OAuth2 1.0.0, conflicting Scalar.AspNetCore versions (2.14.11 vs 2.14.14) These duplicates caused NU1504 build failures (TreatWarningsAsErrors=true). Fix: Remove old Foundatio 7.18.3 + its ProjectReference duplicate, remove NEST.JsonNetSerializer, deduplicate OAuth2 and Scalar (keeping latest).
…exponents RCA: The static ConvertJsonElementNumber helper (called from Event.OnDeserialized for every extension data property) used unconditional element.GetDecimal() for numbers with decimal points or exponents. Decimal has max range ~±7.9×10²⁸, so values like 1e100 (common in telemetry data, scientific measurements, and JS Number.MAX_VALUE) throw OverflowException. The instance ReadNumber method already had the correct TryGetDecimal→double fallback pattern but the static helper did not share this logic — an asymmetry introduced when the static helper was extracted. Fix: Use TryGetDecimal with GetDouble fallback in both the floating-point branch AND the large-integer fallback, matching the instance method's fault tolerance. Add direct unit tests for ConvertJsonElement with out-of-decimal-range values.
RCA: JsonObject.Add throws ArgumentException if the key already exists. The Rename and RenameOrRemoveIfNullOrEmpty methods rebuild the object in property-order and substitute currentName with newName, but never checked whether newName already existed as a separate property. When event upgrade plugins rename a field to a name that malformed client data already populated, this crashed the event pipeline. Principle: Rename semantics should match Newtonsoft JObject behavior where the last-written key wins. In a rename, the source value is the intentional one (being migrated to the canonical name), so it takes precedence. Fix: Skip properties whose key matches newName during the rebuild loop. The renamed value is inserted at the original position, and the pre-existing collision property is dropped. Both Rename and RenameOrRemoveIfNullOrEmpty are patched consistently.
The Dict→JsonNode→String→T path in GetValue<T> performs three serialization steps. The JsonNode intermediate is required for NormalizeKeysForType which renames PascalCase keys in-place to match SnakeCaseLower property names. The final ToJsonString→Deserialize could be eliminated by deserializing directly from the node if ITextSerializer exposed JsonSerializerOptions. Annotated for future optimization (not a correctness issue).
…behavior The test 'Deserialize_JsonWithUnknownRootProperties_IgnoresUnknownProperties' implied that unknown root properties are discarded. In reality, they are captured via [JsonExtensionData] and merged into Event.Data by OnDeserialized(). Rename to 'MergesIntoData' and add assertions verifying the unknown properties are actually present in ev.Data with correct values.
…paths ASP.NET Core's DefaultProblemDetailsFactory adds 'traceId' (camelCase) to ProblemDetails.Extensions. The DefaultProblemDetailsWriter (status code pages) applies PropertyNamingPolicy to extension keys producing 'trace_id', but MVC's SystemTextJsonOutputFormatter writes [JsonExtensionData] keys literally, producing 'traceId'. This caused inconsistent API responses. Fix: explicitly rename the key in CustomizeProblemDetails (called for both paths) so the literal key is always 'trace_id'. Also set ProblemDetails.Instance property directly instead of polluting the Extensions dictionary — Instance is a first-class RFC 9457 member.
The @submission_client.version field reflects the FluentRest assembly version, which was bumped from 11.0.0.0 to 11.1.0.0 as part of the dependency updates.
The PostEvent_WithExtraRootProperties_CapturedInDataBag and PostEvent_WithExtraPropertiesAndKnownData_PreservesAllData tests used conditional assertions (if ContainsKey) that would pass vacuously when data was missing. Made assertions unconditional to properly validate the serialization round-trip. Also added nested object/array assertions in PostEvent_WithExtraComplexProperties_CapturedAsObjects using 42L to match ES long integer behavior.
…lity WebHooksJob now uses dedicated JsonSerializerOptions for webhook delivery that does NOT use DefaultIgnoreCondition.WhenWritingNull or the EmptyCollectionModifier. This ensures external webhook consumers continue receiving null fields (DateFixed, Description, etc.) and empty collections (Tags: []) in the payload — matching the original Newtonsoft.Json behavior. The webhook test was also updated to use the same delivery-path options, and the expected JSON files now reflect the full payload including nulls. For v2 webhooks, this is an additive change (extra null fields) which is safe for consumers that ignore unknown properties.
…Element When TrySetPropertyValue receives a raw JsonElement (e.g., from UnknownProperties in legacy PATCH endpoints), it was calling JsonSerializer.Deserialize without the configured options. This would fail for complex types like Dictionary<string, bool> that rely on the snake_case naming policy. The DeltaJsonConverter now passes options to the Delta instance for use in this fallback path.
…orruption The STJ encoder uses UnicodeRanges.All which passes non-ASCII characters through unescaped (unlike Newtonsoft which escaped to \\uXXXX). The existing Encoding.ASCII.GetBytes would corrupt these characters (e.g., in release notification messages). Also fixes the ArraySegment length parameter which used string char count instead of byte array length.
The test scope naming used 'test' for id=0 and 'test-{N}' for id>0.
When Foundatio's DeleteIndexesAsync/ConfigureIndexesAsync ran with a
'test-*' wildcard, it inadvertently matched indices from 'test-1',
'test-2', etc., causing invalid_alias_name_exception failures.
Changed to 'xtest{N}' format so no scope prefix is a substring of
another, eliminating the race condition during parallel test execution.
totalOrphanedEventCount was declared and logged at the end of both DeleteOrphanedEventsByProjectAsync and DeleteOrphanedEventsByOrganizationAsync but never incremented inside the batch loop. Operators reading the summary log line would always see 'Found 0 orphaned events' even when hundreds were deleted. The deletes themselves were unaffected. Same increment pattern already exists in DeleteOrphanedEventsByStackAsync.
The encoder was described as 'XSS-safe' that 'escapes <, >, &', but JavaScriptEncoder.Create(UnicodeRanges.All) does the opposite — it allows those characters through unescaped. The default encoder escapes them. The actual intent is to allow non-ASCII Unicode (CJK, emoji, etc.) through unescaped for readability. For JSON APIs the Content-Type header is the XSS boundary, not the encoder.
| } | ||
|
|
||
| if (path) { | ||
| if (path && path.indexOf("/") !== 0) { |
| httpOptions.url = httpOptions.url | ||
| ? httpOptions.url | ||
| : joinUrl(this.SatellizerConfig.baseUrl, this.SatellizerConfig.unlinkUrl); | ||
| httpOptions.data = { provider: provider } || httpOptions.data; |
| var map = function (arr, fn) { | ||
| var res = []; | ||
| for (var i = 0, l = arr.length; i < l; i++) { | ||
| res.push(fn(arr[i], i)); |
Comment on lines
+18
to
+69
| controller: function ($scope, notificationService, translateService) { | ||
| var vm = this; | ||
| function copied() { | ||
| notificationService.success(translateService.T("Copied!")); | ||
| } | ||
|
|
||
| function demoteTab() { | ||
| return $scope.demoteTab({ tabName: vm.title }); | ||
| } | ||
|
|
||
| function getData(data, exclusions) { | ||
| exclusions = exclusions && exclusions.length ? exclusions : []; | ||
| if (typeof data !== "object" || !(data instanceof Object)) { | ||
| return data; | ||
| } | ||
|
|
||
| return Object.keys(data) | ||
| .filter(function (value) { | ||
| return value && value.length && exclusions.indexOf(value) < 0; | ||
| }) | ||
| .map(function (value) { | ||
| return { key: value, name: value }; | ||
| }) | ||
| .sort(function (a, b) { | ||
| return a.name - b.name; | ||
| }) | ||
| .reduce(function (a, b) { | ||
| a[b.name] = data[b.key]; | ||
| return a; | ||
| }, {}); | ||
| } | ||
|
|
||
| function promoteTab() { | ||
| return $scope.promoteTab({ tabName: vm.title }); | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| vm.copied = copied; | ||
| vm.canPromote = $scope.canPromote !== false; | ||
| vm.demoteTab = demoteTab; | ||
| vm.data = $scope.data; | ||
| vm.hasData = typeof vm.data !== "undefined" && !angular.equals({}, vm.data); | ||
| vm.data_json = vm.hasData ? angular.toJson(vm.data) : ""; | ||
| vm.filteredData = getData(vm.data, $scope.excludedKeys); | ||
| vm.hasFilteredData = | ||
| typeof vm.filteredData !== "undefined" && !angular.equals({}, vm.filteredData); | ||
| vm.isPromoted = $scope.isPromoted === true; | ||
| vm.promoteTab = promoteTab; | ||
| vm.showRaw = false; | ||
| vm.title = translateService.T($scope.title); | ||
| }; | ||
| }, |
Comment on lines
+4
to
+264
| .submit(); | ||
| if (openInNewTab) { | ||
| $window.open($state.href("app.event", { id: id }, { absolute: true }), "_blank"); | ||
| } else { | ||
| $state.go("app.event", { id: id }); | ||
| } | ||
|
|
||
| event.preventDefault(); | ||
| } | ||
|
|
||
| function nextPage() { | ||
| $ExceptionlessClient | ||
| .createFeatureUsage(vm._source + ".nextPage") | ||
| .setProperty("next", vm.next) | ||
| .submit(); | ||
| return get(vm.next); | ||
| } | ||
|
|
||
| function previousPage() { | ||
| $ExceptionlessClient | ||
| .createFeatureUsage(vm._source + ".previousPage") | ||
| .setProperty("previous", vm.previous) | ||
| .submit(); | ||
| return get(vm.previous); | ||
| } | ||
|
|
||
| function save(action) { | ||
| function onSuccess() { | ||
| vm.selectedIds = []; | ||
| } | ||
|
|
||
| if (vm.selectedIds.length === 0) { | ||
| notificationService.info(null, translateService.T("Please select one or more events")); | ||
| } else { | ||
| action.run(vm.selectedIds).then(onSuccess); | ||
| } | ||
| } | ||
|
|
||
| function updateSelection() { | ||
| if (vm.events && vm.events.length === 0) return; | ||
|
|
||
| if (vm.selectedIds.length > 0) vm.selectedIds = []; | ||
| else { | ||
| vm.selectedIds = vm.events.map(function (event) { | ||
| return event.id; | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function toggleDateSort() { | ||
| vm.settings.sortByDateDescending = !vm.sortByDateDescending; | ||
| vm.sortByDateDescending = vm.settings.sortByDateDescending; | ||
|
|
||
| var options = vm.currentOptions; | ||
| var sortPrefix = vm.sortByDateDescending ? "-" : "+"; | ||
| options.sort = sortPrefix + "date"; | ||
|
|
||
| get(options); | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| vm._source = vm.settings.source + ".events"; | ||
| vm.actions = vm.settings.hideActions ? [] : eventsActionsService.getActions(); | ||
| vm.afterRelativeText = afterRelativeText; | ||
| vm.beforeRelativeText = beforeRelativeText; | ||
| vm.canRefresh = canRefresh; | ||
| vm.currentEventId = vm.settings.eventId; | ||
| vm.currentOptions = null; | ||
| vm.events = []; | ||
| vm.get = get; | ||
| vm.hasFilter = filterService.hasFilter; | ||
| vm.hideSessionStartTime = vm.settings.hideSessionStartTime || false; | ||
| vm.refreshing = true; | ||
| vm.loading = true; | ||
| vm.open = open; | ||
| vm.nextPage = nextPage; | ||
| vm.previousPage = previousPage; | ||
| vm.timeHeaderText = vm.settings.timeHeaderText || "Date"; | ||
| vm.relativeTo = function () { | ||
| return vm.settings.relativeTo; | ||
| }; | ||
| vm.toggleDateSort = toggleDateSort; | ||
| vm.sortByDateDescending = | ||
| vm.settings.sortByDateDescending === undefined ? true : vm.sortByDateDescending; | ||
| vm.save = save; | ||
| vm.selectedIds = []; | ||
| vm.showType = vm.settings.summary | ||
| ? vm.settings.summary.showType | ||
| : !filterService.getEventType(); | ||
| vm.showIPAddress = vm.settings.summary | ||
| ? vm.settings.summary.showIPAddress | ||
| : filterService.getEventType() === "404"; | ||
| vm.updateSelection = updateSelection; | ||
| get(); | ||
| }; | ||
| }, | ||
| ], | ||
| controllerAs: "vm", | ||
| }; | ||
| }); |
Comment on lines
+11
to
+187
| total += data.total || 0; | ||
| }); | ||
|
|
||
| data.company = { | ||
| company_id: currentOrganization.id, | ||
| name: currentOrganization.name, | ||
| remote_created_at: objectIDService.create(currentOrganization.id).timestamp, | ||
| plan: currentOrganization.plan_id, | ||
| monthly_spend: currentOrganization.billing_price, | ||
| billing_changed_by_user_id: currentOrganization.billing_changed_by_user_id, | ||
| total_events: total, | ||
| }; | ||
|
|
||
| if (currentOrganization.subscribe_date) { | ||
| data.company.subscribe_at = moment(currentOrganization.subscribe_date).unix(); | ||
| } | ||
| } | ||
|
|
||
| return data; | ||
| } | ||
|
|
||
| function getOrganizations(canUpdate) { | ||
| function onSuccess(response) { | ||
| vm.organizations = response.data.plain(); | ||
|
|
||
| if (canUpdate === true) { | ||
| updateIntercom(); | ||
| } | ||
|
|
||
| return vm.organizations; | ||
| } | ||
|
|
||
| return organizationService.getAll().then(onSuccess); | ||
| } | ||
|
|
||
| function getProjects(canUpdate) { | ||
| function onSuccess(response) { | ||
| vm.projects = response.data.plain(); | ||
|
|
||
| if (canUpdate === true) { | ||
| updateIntercom(); | ||
| } | ||
|
|
||
| return vm.projects; | ||
| } | ||
|
|
||
| return projectService.getAll().then(onSuccess); | ||
| } | ||
|
|
||
| function getUser(canUpdate) { | ||
| function onSuccess(response) { | ||
| vm.user = response.data.plain(); | ||
|
|
||
| if (canUpdate === true) { | ||
| updateIntercom(); | ||
| } | ||
|
|
||
| return vm.user; | ||
| } | ||
|
|
||
| return userService.getCurrentUser().then(onSuccess); | ||
| } | ||
|
|
||
| function hide() { | ||
| $intercom.hide(); | ||
| } | ||
|
|
||
| function initializeIntercom() { | ||
| return $intercom.boot(getIntercomData()); | ||
| } | ||
|
|
||
| function shutdown() { | ||
| return $intercom.shutdown(); | ||
| } | ||
|
|
||
| function updateIntercom(hideInterface) { | ||
| if (hideInterface === true) { | ||
| hide(); | ||
| } | ||
|
|
||
| return $intercom.update(getIntercomData()); | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| var interval = $interval(updateIntercom, 90000); | ||
| $scope.$on("$destroy", function () { | ||
| $interval.cancel(interval); | ||
| }); | ||
|
|
||
| vm.getOrganizations = getOrganizations; | ||
| vm.getProjects = getProjects; | ||
| vm.getUser = getUser; | ||
| vm.hide = hide; | ||
| vm.IntercomAppId = INTERCOM_APPID; | ||
| vm.shutdown = shutdown; | ||
| vm.updateIntercom = updateIntercom; | ||
|
|
||
| get().then(initializeIntercom); | ||
| }; | ||
| }, |
Comment on lines
+24
to
+242
| // sort object keys longest first, then alphabetically. | ||
| var sortedKeys = Object.keys(configSettings).sort(function (a, b) { | ||
| return b.length - a.length || a.localeCompare(b); | ||
| }); | ||
|
|
||
| for (var index in sortedKeys) { | ||
| var key = sortedKeys[index]; | ||
| if (!startsWith(key.toLowerCase(), sourcePrefix)) { | ||
| continue; | ||
| } | ||
|
|
||
| var cleanKey = key.substring(sourcePrefix.length); | ||
| if (cleanKey.toLowerCase() === vm.source.toLowerCase()) { | ||
| continue; | ||
| } | ||
|
|
||
| // check for wildcard match | ||
| if (isMatch(source, [cleanKey])) { | ||
| return getLogLevel(configSettings[key]); | ||
| } | ||
| } | ||
|
|
||
| return getLogLevel(null); | ||
| } | ||
|
|
||
| function isMatch(input, patterns, ignoreCase) { | ||
| if (typeof input !== "string") { | ||
| return false; | ||
| } | ||
|
|
||
| if (ignoreCase === undefined) { | ||
| ignoreCase = true; | ||
| } | ||
|
|
||
| var trim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; | ||
| input = (ignoreCase ? input.toLowerCase() : input).replace(trim, ""); | ||
|
|
||
| return (patterns || []).some(function (pattern) { | ||
| if (typeof pattern !== "string") { | ||
| return false; | ||
| } | ||
|
|
||
| pattern = (ignoreCase ? pattern.toLowerCase() : pattern).replace(trim, ""); | ||
| if (pattern.length <= 0) { | ||
| return false; | ||
| } | ||
|
|
||
| var startsWithWildcard = pattern[0] === "*"; | ||
| if (startsWithWildcard) { | ||
| pattern = pattern.slice(1); | ||
| } | ||
|
|
||
| var endsWithWildcard = pattern[pattern.length - 1] === "*"; | ||
| if (endsWithWildcard) { | ||
| pattern = pattern.substring(0, pattern.length - 1); | ||
| } | ||
|
|
||
| if (startsWithWildcard && endsWithWildcard) { | ||
| return pattern.length <= input.length && input.indexOf(pattern, 0) !== -1; | ||
| } | ||
|
|
||
| if (startsWithWildcard) { | ||
| return endsWith(input, pattern); | ||
| } | ||
|
|
||
| if (endsWithWildcard) { | ||
| return startsWith(input, pattern); | ||
| } | ||
|
|
||
| return input === pattern; | ||
| }); | ||
| } | ||
|
|
||
| function startsWith(input, prefix) { | ||
| return input.substring(0, prefix.length) === prefix; | ||
| } | ||
|
|
||
| function endsWith(input, suffix) { | ||
| return input.indexOf(suffix, input.length - suffix.length) !== -1; | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| vm.canRefresh = canRefresh; | ||
| vm.loading = true; | ||
| vm.level = null; | ||
| vm.defaultLevel = null; | ||
| vm.get = get; | ||
| vm.projectId = $scope.projectId; | ||
| vm.setLogLevel = setLogLevel; | ||
| vm.setDefaultLogLevel = setDefaultLogLevel; | ||
| vm.source = $scope.source || ""; | ||
|
|
||
| $scope.$watch("projectId", function (projectId) { | ||
| if (projectId) { | ||
| vm.projectId = projectId; | ||
| return get(); | ||
| } | ||
| }); | ||
| }; | ||
| }, |
Comment on lines
+21
to
+298
| if (!filter || organization.name.toLocaleLowerCase().includes(filter)) | ||
| return organization; | ||
|
|
||
| var hasProjectMatchingFilter = vm.projects.find(function (p) { | ||
| return ( | ||
| p.organization_id === organization.id && | ||
| p.name.toLocaleLowerCase().includes(filter) | ||
| ); | ||
| }); | ||
|
|
||
| if (hasProjectMatchingFilter) return organization; | ||
|
|
||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| function getFilteredProjectsByOrganizationId(id) { | ||
| var filter = vm.filter && vm.filter.toLocaleLowerCase(); | ||
|
|
||
| return vm.projects.filter(function (project) { | ||
| if (project.organization_id !== id) return null; | ||
|
|
||
| if ( | ||
| !filter || | ||
| project.name.toLocaleLowerCase().includes(filter) || | ||
| project.organization_name.toLocaleLowerCase().includes(filter) | ||
| ) | ||
| return project; | ||
|
|
||
| return null; | ||
| }); | ||
| } | ||
|
|
||
| function getStateName() { | ||
| if ($state.current.name.endsWith("frequent")) { | ||
| return "frequent"; | ||
| } | ||
|
|
||
| if ($state.current.name.endsWith("new")) { | ||
| return "new"; | ||
| } | ||
|
|
||
| if ($state.current.name.endsWith("users")) { | ||
| return "users"; | ||
| } | ||
|
|
||
| return "events"; | ||
| } | ||
|
|
||
| function isOnSessionEvents() { | ||
| return ( | ||
| $state.current.name.contains("app.session-") || | ||
| $state.current.name === "app.session.events" | ||
| ); | ||
| } | ||
|
|
||
| function isOnReports() { | ||
| return $state.current.name.contains("app.reports."); | ||
| } | ||
|
|
||
| function showSearch() { | ||
| return vm.projects.length >= 20 || vm.organizations.length >= 20; | ||
| } | ||
|
|
||
| function update() { | ||
| vm.filteredDisplayName = getFilterName(); | ||
| vm.urls = buildUrls(); | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| var updateFilterDropDownMaxHeight = debounce(function () { | ||
| vm.filterDropDownMaxHeight = angular.element($window).height() - 100; | ||
| }, 150); | ||
|
|
||
| var window = angular.element($window); | ||
| window.bind("resize", updateFilterDropDownMaxHeight); | ||
|
|
||
| $rootScope.$on("$stateChangeSuccess", update); | ||
| var unbind = $scope.$on("$destroy", function () { | ||
| unbind(); | ||
| window.unbind("resize", updateFilterDropDownMaxHeight); | ||
| }); | ||
|
|
||
| vm.filteredDisplayName = "Loading"; | ||
| vm.get = get; | ||
| vm.getFilteredOrganizations = getFilteredOrganizations; | ||
| vm.getFilteredProjectsByOrganizationId = getFilteredProjectsByOrganizationId; | ||
| vm.filter = ""; | ||
| vm.isLoadingOrganizations = true; | ||
| vm.isLoadingProjects = true; | ||
| vm.organizations = []; | ||
| vm.projects = []; | ||
| vm.showSearch = showSearch; | ||
| vm.urls = buildUrls(); | ||
| vm.update = update; | ||
|
|
||
| updateFilterDropDownMaxHeight(); | ||
| get(); | ||
| }; | ||
| }, |
Comment on lines
+9
to
+25
| controller: function ($window) { | ||
| var rvm = this; | ||
| function processReleaseNotification(notification) { | ||
| if (notification) { | ||
| if (notification.critical) { | ||
| $window.location.reload(); | ||
| } | ||
|
|
||
| rvm.releaseNotificationMessage = notification.message; | ||
| } | ||
| } | ||
|
|
||
| this.$onInit = function $onInit() { | ||
| rvm.processReleaseNotification = processReleaseNotification; | ||
| rvm.releaseNotificationMessage = ""; | ||
| }; | ||
| }, |
Comment on lines
+6
to
+115
| frames += "<div>--- End of inner exception stack trace ---</div>"; | ||
| } | ||
|
|
||
| frames += "</div>"; | ||
| } else { | ||
| frames += stackTrace.replace(" ", ""); | ||
|
|
||
| if (index < exceptions.length - 1) { | ||
| frames += "--- End of inner exception stack trace ---"; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return frames; | ||
| } | ||
|
|
||
| function buildStackTrace(exceptions, includeHTML) { | ||
| return ( | ||
| buildStackTraceHeader(exceptions, includeHTML) + buildStackFrames(exceptions.reverse(), includeHTML) | ||
| ); | ||
| } | ||
|
|
||
| function buildStackTraceHeader(exceptions, includeHTML) { | ||
| var header = ""; | ||
| for (var index = 0; index < exceptions.length; index++) { | ||
| if (includeHTML) { | ||
| header += '<span class="ex-header">'; | ||
| } | ||
|
|
||
| if (index > 0) { | ||
| header += " ---> "; | ||
| } | ||
|
|
||
| var hasType = !!exceptions[index].type; | ||
| if (hasType) { | ||
| if (includeHTML) { | ||
| header += '<span class="ex-type">' + escapeHTML(exceptions[index].type) + "</span>: "; | ||
| } else { | ||
| header += exceptions[index].type + ": "; | ||
| } | ||
| } | ||
|
|
||
| if (exceptions[index].message) { | ||
| if (includeHTML) { | ||
| header += '<span class="ex-message">' + escapeHTML(exceptions[index].message) + "</span>"; | ||
| } else { | ||
| header += exceptions[index].message; | ||
| } | ||
| } | ||
|
|
||
| if (hasType) { | ||
| if (includeHTML) { | ||
| header += "</span>"; | ||
| } else { | ||
| header += "\r\n"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return header; | ||
| } | ||
|
|
||
| function escapeHTML(input) { | ||
| if (!input || !input.replace) { | ||
| return input; | ||
| } | ||
|
|
||
| return $sce.trustAsHtml( | ||
| input | ||
| .replace(/&/g, "&") | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">") | ||
| .replace(/"/g, """) | ||
| .replace(/'/g, "'") | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| bindToController: true, | ||
| restrict: "E", | ||
| replace: true, | ||
| scope: { | ||
| exception: "=", | ||
| textStackTrace: "=?", | ||
| }, | ||
| template: '<pre class="stack-trace"><code ng-bind-html="vm.stackTrace"></code></pre>', | ||
| controller: [ | ||
| function () { | ||
| var vm = this; | ||
| this.$onInit = function $onInit() { | ||
| var errors = simpleErrorService.getExceptions(vm.exception); | ||
| vm.stackTrace = $sce.trustAsHtml(buildStackTrace(errors, true)); | ||
| vm.textStackTrace = buildStackTrace(errors, false); | ||
| }; | ||
| }, | ||
| ], | ||
| controllerAs: "vm", | ||
| }; | ||
| }); |
| var projectResults = await _projectRepository.GetByOrganizationIdAsync(organization.Id, o => o.SoftDeleteMode(SoftDeleteQueryMode.All).SearchAfterPaging().PageLimit(100)); | ||
| _logger.LogInformation("Updating usage for {ProjectTotal} projects(s)", projectResults.Total); | ||
|
|
||
| var sw = Stopwatch.StartNew(); |
Add 21 new tests across two test classes covering CleanupDataJob and CleanupOrphanedDataJob with multi-tenant scenarios: CleanupDataJobTests (9 new): - Suspended token cleanup across multiple tenants - Soft-deleted organization/project/stack cleanup isolation - Retention period enforcement for free vs paid plans - Combined multi-tenant full cleanup scenario CleanupOrphanedDataJobTests (12 new): - Orphaned events by stack/project/organization deletion - Large volume test (5000 valid + 10000 orphaned events) - Cross-tenant isolation verification - Duplicate stack detection and cleanup - Empty database edge case - Combined all-orphan-types scenario fix: preserve webhook backwards compatibility for V1 and V2 payloads The old Newtonsoft code used DynamicTypeContractResolver which applied different serialization rules per type: - V1 (VersionOneWebHookEvent/Stack): default resolver — PascalCase via [JsonProperty], includes nulls and empty collections. - V2 (WebHookEvent/Stack): LowerCaseUnderscorePropertyNamesContractResolver — snake_case naming, omits nulls and empty collections. Replicate this in STJ with two options sets: - V1: clone standard options + DefaultIgnoreCondition.Never + DefaultJsonTypeInfoResolver (no EmptyCollectionModifier) - V2: clone standard options as-is (WhenWritingNull + EmptyCollectionModifier + SnakeCaseLower naming) Pick options at serialization time based on the runtime type of the webhook data object. fix: correct misleading encoder comment — HTML chars ARE escaped JavaScriptEncoder.Create(UnicodeRanges.All) allows non-ASCII through but still escapes HTML-sensitive characters (<, >, &, ') to their \\uXXXX forms. The old comment incorrectly claimed they were NOT escaped. Test results confirm: & serializes as \\u0026. fix: adapt test infrastructure for Elastic.Clients.Elasticsearch v8 The new Elastic.Clients.Elasticsearch v8 client differs from NEST in several ways that broke test isolation: - Does NOT split comma-separated index strings (DeleteByQuery targeted nothing) - Concurrent index creation races during parallel test runs - AppScope pool ID returned before host fully disposed (alias conflicts) - ConfigureIndexesAsync cache prevented recreation after wildcard delete Fix: use scope wildcard patterns for cleanup, add retry logic for alias races, scope index configuration cache by ID, return pool IDs after full disposal, and aggressively delete all indices for a scope on test init. refactor: remove #region directives from test files Regions add visual clutter without providing meaningful organization. Test methods are already grouped by class and attribute-driven discovery makes region-based grouping unnecessary. Fix ArgumentException when multiple stacks share the same SignatureHash in cache Dictionary.Add throws ArgumentException on duplicate keys. When multiple stacks share the same SignatureHash (e.g. duplicate stacks created with same project/type/source), AddDocumentsToCacheAsync would throw. Use indexer assignment instead so the last write wins gracefully. Fix WillExcludeOldStacksForStackNewMode test - give log events unique sources Events 1, 2, and 3 all defaulted to Type=Log and Source='Test Event', giving them identical DuplicateSignature values. The FixDuplicateStacks logic (run periodically) merges stacks with the same DuplicateSignature, marking all but one as is_deleted=true. This caused event 3's stack to be soft-deleted, returning only 1 result instead of 2. Fix: assign unique Source values to each log event so they have distinct stacks that won't be merged by the deduplication logic. fix: DeltaJsonConverter respects [JsonPropertyName], removes itself from options copy; Delta uses TryGetValue Add serialization audit tests for STJ migration verification Comprehensive integration tests that submit events, webhooks, and patches with different JSON property casings (snake_case, camelCase, PascalCase, MIXED) and capture request/elastic/response JSON to audit-output/ for cross-branch diffing. Key findings documented via test output: - Multi-word PascalCase/camelCase root properties (referenceId, ReferenceId) do not match snake_case wire names and fall into Data bag - Single-word properties match case-insensitively with any casing - NormalizeKeysForType in DataDictionaryExtensions correctly handles multi-word keys for typed models in the Data dictionary - Character escaping: &→\\u0026, +→\\u002B, <→\\u003C, >→\\u003E in output - Webhook creation requires snake_case property names (camelCase returns 400) Add audit-output/ to .gitignore Add casing compatibility and integration tests for STJ migration Reproduce behavioral differences found during serialization audit: - PascalCase/camelCase ReferenceId not binding (goes to ExtensionData) - Date-only strings being parsed as DateTimeOffset - Numeric precision, empty collection omission edge cases These tests confirm the bugs before fixes are applied. Fix PascalCase/camelCase ReferenceId binding in STJ deserialization STJ's PropertyNameCaseInsensitive with SnakeCaseLower policy only matches case-insensitively against the policy-transformed name (reference_id), so PascalCase (ReferenceId) and camelCase (referenceId) end up in ExtensionData. Add fallback in OnDeserialized to check ExtensionData for these alternate casings and bind to the ReferenceId property, preserving backwards compatibility with payloads submitted by older SDKs. Preserve date-only strings instead of parsing as DateTimeOffset Date-only strings like '2026-01-15' should not be expanded to DateTimeOffset values (e.g., '2026-01-15T00:00:00-06:00') because: 1. They may not represent dates in user data 2. The timezone expansion adds information that wasn't in the original 3. This matches the legacy Newtonsoft behavior (DateParseHandling.None) Only parse strings as dates when they contain a 'T' time separator, indicating an explicit ISO 8601 datetime with time component. Add serialization-audit skill for branch comparison workflow Documents the workflow for generating, diffing, and testing serialization behavior across branches during serializer migrations. Clean up STJ migration: fix date parsing, add empty collection tests, document ReferenceId - Remove lowercase 't' check in date parsing (ISO 8601 uses uppercase T only) - Add tests for empty collection omission (tags, references, roundtrip) - Remove decorative comment borders from test/script files - Fix serialization-audit skill YAML frontmatter for discoverability - Document ReferenceId fallback with SDK version context
| organization.SuspensionNotes = "blah"; | ||
| await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); | ||
|
|
||
| var project = await _projectRepository.AddAsync(_projectData.GenerateSampleProject()); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace JSON.NET with System.Text.Json
Replaces all Newtonsoft.Json serialization with System.Text.Json (STJ) and migrates from NEST to Elastic.Clients.Elasticsearch.
Key changes
Serialization
SnakeCaseLowernaming policyObjectToInferredTypesConverterhandlesobject-typed properties (replacesDataObjectConverter)[JsonExtensionData]onEventmerges root-level@error,@request, etc. intoDatadict[JsonPropertyName]overrides for legacy property names (o_s_name,o_s_version)[JsonPropertyName]for PascalCase backward compatibilityElasticsearch
Elastic.Clients.ElasticsearchthroughoutEvent processing
JObjecttoJsonObject(System.Text.Json.Nodes)GetValue<T>()simplified — removed unnecessaryTryDeserializeWithFallback(PascalCase data never existed in ES)ErrorPlugin/SimpleErrorPluginafter pipeline mutationRemoved
DataObjectConverter,ElasticJsonNetSerializer, all Newtonsoft classesFoundatio.JsonNet,NEST.JsonNetSerializer,FluentRest.NewtonsoftJsonpackagesTest results