diff --git a/CHANGES b/CHANGES index 81a69e445a372c..b86be93b966255 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,443 @@ +25.11.1 +------- + +### New Features ✨ + +- feat(traces): Add cross event dropdown functionality by @nsdeschenes in [#104100](https://github.com/getsentry/sentry/pull/104100) +- feat: Set Default max pickable days by @Zylphrex in [#104109](https://github.com/getsentry/sentry/pull/104109) +- feat(symbols): Add platform-restricted builtin symbol sources with org access control by @vaind in [#102013](https://github.com/getsentry/sentry/pull/102013) +- feat(alerts): Allow bumping max snuba subscription limit by @shruthilayaj in [#104126](https://github.com/getsentry/sentry/pull/104126) +- feat(dashboards): add details widget to query summary page by @DominikB2014 in [#104112](https://github.com/getsentry/sentry/pull/104112) +- feat(preprod): Add second row with build number, version info and date to comparison screen (EME-520) by @runningcode in [#104117](https://github.com/getsentry/sentry/pull/104117) +- feat(symbols): Pass project platform to builtin symbol sources API (frontend) by @vaind in [#104116](https://github.com/getsentry/sentry/pull/104116) +- feat(dashboards): Add duplicate control for prebuilt dashboards by @DominikB2014 in [#104103](https://github.com/getsentry/sentry/pull/104103) +- feat(dashboards): Prevents rendering edit and save ui on prebuilt insights dashboards by @edwardgou-sentry in [#104101](https://github.com/getsentry/sentry/pull/104101) +- feat(dashboards): Add wildcard operators to global filters by @Ahmed-Labs in [#104070](https://github.com/getsentry/sentry/pull/104070) +- feat(dashboards): add details widget type by @DominikB2014 in [#104059](https://github.com/getsentry/sentry/pull/104059) +- feat(dashboards): Add edit and delete guard for prebuilt dashboards to backend dashboard details endpoint by @edwardgou-sentry in [#104098](https://github.com/getsentry/sentry/pull/104098) +- feat(dashboards): misc fixes to query prebuilt dashboards by @DominikB2014 in [#104099](https://github.com/getsentry/sentry/pull/104099) +- feat(dashboards): register "details" widget as valid widget on the backend by @DominikB2014 in [#104062](https://github.com/getsentry/sentry/pull/104062) +- feat(vitals): Add analytics for primary and secondary release selection by @markushi in [#103960](https://github.com/getsentry/sentry/pull/103960) +- feat(onboarding): Add log drain docs to logs onboarding by @AbhiPrasad in [#104084](https://github.com/getsentry/sentry/pull/104084) +- feat(playstation): Always fetch dumps from tempest by @mujacica in [#104040](https://github.com/getsentry/sentry/pull/104040) +- feat(objectstore): Improve host rewriting by @lcian in [#103964](https://github.com/getsentry/sentry/pull/103964) +- feat(agents): Show agent names in traces table by @ArthurKnaus in [#104079](https://github.com/getsentry/sentry/pull/104079) +- feat: Remove adjacent tracing feature flag by @JPeer264 in [#103891](https://github.com/getsentry/sentry/pull/103891) +- feat(eap): Support reading boolean attributes from response by @phacops in [#104063](https://github.com/getsentry/sentry/pull/104063) +- feat(dashboards): Updates prebuilt dashboard preview and management in the All Dashboards view by @edwardgou-sentry in [#104018](https://github.com/getsentry/sentry/pull/104018) +- feat(vercel): Remove `get_env_var_map` helper by @AbhiPrasad in [#104068](https://github.com/getsentry/sentry/pull/104068) +- feat(explore): Hook to respect new downsampled retention by @Zylphrex in [#104013](https://github.com/getsentry/sentry/pull/104013) +- feat(vercel): Add drain env variables to Vercel integration by @AbhiPrasad in [#103986](https://github.com/getsentry/sentry/pull/103986) +- feat(issues): Allow disabling of filters in top group demo, add tags by @scttcper in [#104037](https://github.com/getsentry/sentry/pull/104037) +- feat(aci): redirect incidents to metric issue details by @ameliahsu in [#104001](https://github.com/getsentry/sentry/pull/104001) +- feat(crons): Always create detectors for all monitors in MonitorValidator by @evanpurkhiser in [#104004](https://github.com/getsentry/sentry/pull/104004) +- feat(preprod): record if the status check failed by @trevor-e in [#104039](https://github.com/getsentry/sentry/pull/104039) +- feat(explorer): rpcs for getting log/metric attrs for a trace id + substring by @aliu39 in [#103875](https://github.com/getsentry/sentry/pull/103875) +- feat(aci): Format percentage based thresholds/data by @scttcper in [#104035](https://github.com/getsentry/sentry/pull/104035) +- feat(explore): Enabling searching attributes on group by by @nsdeschenes in [#104003](https://github.com/getsentry/sentry/pull/104003) +- feat(dashboards): loosen unique title db constraint on dashboards by @DominikB2014 in [#104046](https://github.com/getsentry/sentry/pull/104046) +- feat(issues): allow pasting json top issues by @cvxluo in [#104022](https://github.com/getsentry/sentry/pull/104022) +- feat(billing): use Subscription.orgRetention in customerOverview by @vbro in [#103124](https://github.com/getsentry/sentry/pull/103124) +- feat(issues): more functionality for different team viewing in top issues by @cvxluo in [#103947](https://github.com/getsentry/sentry/pull/103947) +- feat(dashboards): Adds action menu to prebuilt dashboard widgets by @edwardgou-sentry in [#103976](https://github.com/getsentry/sentry/pull/103976) +- feat(aci): Move threshold info into detect section of metric monitor details by @malwilley in [#103926](https://github.com/getsentry/sentry/pull/103926) +- feat(logs): Without chart data when table is empty by @Zylphrex in [#103930](https://github.com/getsentry/sentry/pull/103930) +- feat(deletions): Retry timed out tasks by @armenzg in [#103966](https://github.com/getsentry/sentry/pull/103966) +- feat(explore): Add hooks for date page filter props based on data cat… by @Zylphrex in [#103931](https://github.com/getsentry/sentry/pull/103931) +- feat(billing): Update quota endpoints for Seer by @brendanhsentry in [#103948](https://github.com/getsentry/sentry/pull/103948) +- feat: no-token-import rule by @TkDodo in [#103889](https://github.com/getsentry/sentry/pull/103889) +- feat(settings): Remove vercel log drain feature flag by @AbhiPrasad in [#103940](https://github.com/getsentry/sentry/pull/103940) +- feat(dashboards): ensure global filters carryforward when clicking view span samples on table by @DominikB2014 in [#103860](https://github.com/getsentry/sentry/pull/103860) +- feat(devservices): Add symbolicator-tests mode by @loewenheim in [#103959](https://github.com/getsentry/sentry/pull/103959) +- feat(aci): Display apdex option for spans by @scttcper in [#103791](https://github.com/getsentry/sentry/pull/103791) +- feat(tests): Add helper to count mock calls by @lobsterkatie in [#103941](https://github.com/getsentry/sentry/pull/103941) +- feat(explorer): send interactivity flag in client by @roaga in [#103934](https://github.com/getsentry/sentry/pull/103934) +- feat(grouping): Add options to control grouphash caching by @lobsterkatie in [#103943](https://github.com/getsentry/sentry/pull/103943) +- feat(eap): Always log the rpc query instead of only at debug by @wmak in [#103922](https://github.com/getsentry/sentry/pull/103922) +- feat(issues): add summary to top issue card by @cvxluo in [#103880](https://github.com/getsentry/sentry/pull/103880) +- feat(Replay): Update Query Archived Alias by @cliffordxing in [#103914](https://github.com/getsentry/sentry/pull/103914) +- feat(tracemetrics): Copy previous metric instead of using defaults by @Zylphrex in [#103917](https://github.com/getsentry/sentry/pull/103917) +- feat(explore): Add hooks for date page filter props based on data cat… by @Zylphrex in [#103822](https://github.com/getsentry/sentry/pull/103822) +- feat: migrations should noop faster by @joshuarli in [#103795](https://github.com/getsentry/sentry/pull/103795) +- feat(explorer): add config for intelligence level to client by @roaga in [#103873](https://github.com/getsentry/sentry/pull/103873) +- feat: Add support for extrapolation modes in entity subscription by @shruthilayaj in [#103834](https://github.com/getsentry/sentry/pull/103834) +- feat(aci): Replace detector type in url, add default by @scttcper in [#103837](https://github.com/getsentry/sentry/pull/103837) +- feat(replay): Add sticky header support to replay table and header components by @jerryzhou196 in [#103825](https://github.com/getsentry/sentry/pull/103825) +- feat(aci): redirect alert rules to detectors by @ameliahsu in [#103682](https://github.com/getsentry/sentry/pull/103682) +- feat(charts): Adding new chart range selection hook by @Abdkhan14 in [#103748](https://github.com/getsentry/sentry/pull/103748) +- feat(prevent): Remove seer app bullet from onboarding by @suejung-sentry in [#103421](https://github.com/getsentry/sentry/pull/103421) +- feat(deletions): Retry task on timeout by @armenzg in [#103894](https://github.com/getsentry/sentry/pull/103894) +- feat(replay): Add loading state to ReplayPlaylistProvider and update usage in related components by @jerryzhou196 in [#103835](https://github.com/getsentry/sentry/pull/103835) +- feat(sdk): Trial higher envelope serialization limits by @alexander-alderman-webb in [#103882](https://github.com/getsentry/sentry/pull/103882) +- feat(deletions): Cleanup more groups per project by @armenzg in [#103851](https://github.com/getsentry/sentry/pull/103851) +- feat(perforce): Add Perforce integration infrastructure and stubs by @mujacica in [#103287](https://github.com/getsentry/sentry/pull/103287) +- feat(perforce): Add frontend support for Perforce integration by @mujacica in [#103172](https://github.com/getsentry/sentry/pull/103172) +- feat(symbolication): Make frame order explicit by @loewenheim in [#103638](https://github.com/getsentry/sentry/pull/103638) +- feat(issues): top issues experiment by @cvxluo in [#103773](https://github.com/getsentry/sentry/pull/103773) +- feat(aci): add Incident to GroupOpenPeriod lookup endpoint by @ameliahsu in [#103782](https://github.com/getsentry/sentry/pull/103782) +- feat(dashboards): automatically populate dashboard ids in prebuilt dashboards by @DominikB2014 in [#103738](https://github.com/getsentry/sentry/pull/103738) +- feat(charts): Custom icons for legends by @evanpurkhiser in [#103800](https://github.com/getsentry/sentry/pull/103800) +- feat(Replay): Strip Dash Before EAP Replay Query by @cliffordxing in [#103793](https://github.com/getsentry/sentry/pull/103793) +- feat(insights): Adds an endpoint to manually start issue detection task on a project by @edwardgou-sentry in [#103760](https://github.com/getsentry/sentry/pull/103760) +- feat(aci): Add serialized data source to issue occurrence evidence data by @malwilley in [#103549](https://github.com/getsentry/sentry/pull/103549) +- feat(explore): Limit explore visualizes to 8 by @Zylphrex in [#103826](https://github.com/getsentry/sentry/pull/103826) +- feat(aci): Edit connected alerts directly from monitor details by @malwilley in [#103757](https://github.com/getsentry/sentry/pull/103757) +- feat: Remove extrapolation modes from alerts translation by @shruthilayaj in [#103823](https://github.com/getsentry/sentry/pull/103823) +- feat(deletions): Schedule task to delete pending deletions groups by @armenzg in [#103820](https://github.com/getsentry/sentry/pull/103820) +- feat(aci): Make Workflow.when_condition_group unique by @kcons in [#103768](https://github.com/getsentry/sentry/pull/103768) +- feat: Use extrapolation mode in alerts by @shruthilayaj in [#103731](https://github.com/getsentry/sentry/pull/103731) +- feat: Add url_names option to deprecated() by @markstory in [#103758](https://github.com/getsentry/sentry/pull/103758) +- feat(dashboards): add legend to queries module charts by @DominikB2014 in [#103752](https://github.com/getsentry/sentry/pull/103752) +- feat(prevent): Enable EU for ai code review by @suejung-sentry in [#103420](https://github.com/getsentry/sentry/pull/103420) +- feat(dashboards): allow a single equation on a chart by @bcoe in [#103783](https://github.com/getsentry/sentry/pull/103783) +- feat: Add modal for binaries without dsyms by @noahsmartin in [#103554](https://github.com/getsentry/sentry/pull/103554) +- feat(explore): Add saved queries to title for logs and metrics by @nsdeschenes in [#103644](https://github.com/getsentry/sentry/pull/103644) +- feat(objectstore): Add a (no-op) Objectstore endpoint by @lcian in [#103468](https://github.com/getsentry/sentry/pull/103468) +- feat(settings): Remove deprecated route props from SentryApplicationDetails by @scttcper in [#103797](https://github.com/getsentry/sentry/pull/103797) +- feat(billing): update org retention settings in \_admin by @vbro in [#103126](https://github.com/getsentry/sentry/pull/103126) +- feat(issues): top issues ui flag by @cvxluo in [#103776](https://github.com/getsentry/sentry/pull/103776) +- feat(billing): add orgRetention to Subscription type by @vbro in [#103118](https://github.com/getsentry/sentry/pull/103118) +- feat(eventstream): Synchronously write occurrences to EAP from the `SnubaEventStream` by @shashjar in [#103566](https://github.com/getsentry/sentry/pull/103566) +- feat(events): Add logging to debug queries by @wmak in [#103766](https://github.com/getsentry/sentry/pull/103766) +- feat(events): Add rpc request even on error by @wmak in [#103678](https://github.com/getsentry/sentry/pull/103678) +- feat(insights): Remove Web Vital issue detection project allow list and add batching by @edwardgou-sentry in [#103740](https://github.com/getsentry/sentry/pull/103740) +- feat(insights): Updates Web Vital Issue detection titles by @edwardgou-sentry in [#103657](https://github.com/getsentry/sentry/pull/103657) +- feat(preprod): Add codesigning type to check for updates filter by @noahsmartin in [#103727](https://github.com/getsentry/sentry/pull/103727) +- feat(explore): Update confidence footer again by @Zylphrex in [#103680](https://github.com/getsentry/sentry/pull/103680) +- feat(scraps): add tokens by @natemoo-re in [#103685](https://github.com/getsentry/sentry/pull/103685) +- feat(settings): Disable AI settings when gen-ai-features flag is off by @JoshFerge in [#103387](https://github.com/getsentry/sentry/pull/103387) +- feat(aci): Add monitor created/updated analytics by @scttcper in [#103279](https://github.com/getsentry/sentry/pull/103279) +- feat(eap): Enable deletion from EAP by default, with a killswitch option by @shashjar in [#102808](https://github.com/getsentry/sentry/pull/102808) +- feat(explorer): support caseInsensitive param by @aliu39 in [#103494](https://github.com/getsentry/sentry/pull/103494) +- feat(deletions): Re-attempt deletions after 6 hours by @armenzg in [#103643](https://github.com/getsentry/sentry/pull/103643) +- feat(aci): redirect rules to automations with UI FF by @ameliahsu in [#103322](https://github.com/getsentry/sentry/pull/103322) +- feat(insights): Adds metrics to web vitals issue detection task by @edwardgou-sentry in [#103538](https://github.com/getsentry/sentry/pull/103538) +- feat(dashboards): link query overview to summary by @DominikB2014 in [#103530](https://github.com/getsentry/sentry/pull/103530) +- feat(ACI): Delete data in Seer when dynamic detector type is changed by @ceorourke in [#103323](https://github.com/getsentry/sentry/pull/103323) +- feat(dashboards): query dashboards by prebuilt id in api by @DominikB2014 in [#103557](https://github.com/getsentry/sentry/pull/103557) +- feat(seer): better surface coding agent integration errors by @jennmueng in [#103302](https://github.com/getsentry/sentry/pull/103302) +- feat(traces): Set page title to saved query name by @nsdeschenes in [#103633](https://github.com/getsentry/sentry/pull/103633) +- feat(encryption): Add metrics for encrypted field by @vgrozdanic in [#103630](https://github.com/getsentry/sentry/pull/103630) +- feat(encryption): Use EncryptedCharField in TempestCredentials by @vgrozdanic in [#103515](https://github.com/getsentry/sentry/pull/103515) +- feat(aci): Add basic automation analytics by @scttcper in [#103416](https://github.com/getsentry/sentry/pull/103416) +- feat(aci): Ensure DetectorGroup for recurring Groups by @kcons in [#103419](https://github.com/getsentry/sentry/pull/103419) +- feat(detectors): Add detector type to breadcrumbs by @evanpurkhiser in [#103560](https://github.com/getsentry/sentry/pull/103560) +- feat(aci): Disable error monitor create button by @malwilley in [#103544](https://github.com/getsentry/sentry/pull/103544) +- feat(insights): register query module prebuilt dashboards by @DominikB2014 in [#103531](https://github.com/getsentry/sentry/pull/103531) +- feat(aci): Allow empty monitor/alert names by @scttcper in [#103350](https://github.com/getsentry/sentry/pull/103350) +- feat(aci): Rewire insights crons/uptime links to go to Monitors by @malwilley in [#103129](https://github.com/getsentry/sentry/pull/103129) +- feat(spans): Allow integers in count if conditions by @Zylphrex in [#103526](https://github.com/getsentry/sentry/pull/103526) +- feat(encrryption): Add Fernet Key Store by @vgrozdanic in [#103511](https://github.com/getsentry/sentry/pull/103511) +- feat(ai): Normalize model names for a better cost calculation by @vgrozdanic in [#103508](https://github.com/getsentry/sentry/pull/103508) +- feat(preprod): add cards to build details by @mtopo27 in [#103470](https://github.com/getsentry/sentry/pull/103470) +- feat(seer): Add separate scanner acknowledgement function with rollout rate by @JoshFerge in [#103496](https://github.com/getsentry/sentry/pull/103496) +- feat(explorer): copy for metric viewing tool by @roaga in [#103430](https://github.com/getsentry/sentry/pull/103430) +- feat(config): Remove log for tz mismatch by @billyvg in [#103492](https://github.com/getsentry/sentry/pull/103492) +- feat(explorer): highlight nav links and smooth scroll by @roaga in [#103432](https://github.com/getsentry/sentry/pull/103432) +- feat(ui): Disable Amplitude console logging by @billyvg in [#103452](https://github.com/getsentry/sentry/pull/103452) +- feat(onboarding): Simplify metrics onboarding for the Python SDK by @alexander-alderman-webb in [#103354](https://github.com/getsentry/sentry/pull/103354) +- feat(explorer): add rpc to wrap trace-items endpoint by @roaga in [#103429](https://github.com/getsentry/sentry/pull/103429) +- feat(Replay): Add Flag for EAP Query for Replay Details Page by @cliffordxing in [#103278](https://github.com/getsentry/sentry/pull/103278) +- feat(billing): Add notification setting for seer users by @brendanhsentry in [#103411](https://github.com/getsentry/sentry/pull/103411) +- feat: Add feature flag for downsampled date page filter by @Zylphrex in [#103403](https://github.com/getsentry/sentry/pull/103403) +- feat(dashboards): Use a single value selector for boolean filters by @Ahmed-Labs in [#102792](https://github.com/getsentry/sentry/pull/102792) +- feat: Consolidate adjacent traces & move next to search by @JPeer264 in [#102472](https://github.com/getsentry/sentry/pull/102472) + +### Bug Fixes 🐛 + +- fix(snuba): add missing api.metrics.totals.second_query to Referrer enum by @constantinius in [#104155](https://github.com/getsentry/sentry/pull/104155) +- fix(snuba): missing referrer enum for `api.organization-events.metrics-enhanced` and `api.insights.landing-table.metrics-enhanced.primary` by @aldy505 in [#102122](https://github.com/getsentry/sentry/pull/102122) +- fix(playstation): Show warning when connection is good, but no errors by @mujacica in [#104097](https://github.com/getsentry/sentry/pull/104097) +- fix(playstation): Show warning when connection is good, but no errors by @mujacica in [#104096](https://github.com/getsentry/sentry/pull/104096) +- fix(dashboards): Filter out prebuilt dashboards from add to dashboard dropdown options by @edwardgou-sentry in [#104132](https://github.com/getsentry/sentry/pull/104132) +- fix(sdk): Remove higher envelope serialization limits by @alexander-alderman-webb in [#104118](https://github.com/getsentry/sentry/pull/104118) +- fix(explore): Bandaid to escape brackets by @nsdeschenes in [#104108](https://github.com/getsentry/sentry/pull/104108) +- fix(dashboards): Fix trigger for single value global filter selector by @Ahmed-Labs in [#104107](https://github.com/getsentry/sentry/pull/104107) +- fix(dashboards): can't save details widget by @DominikB2014 in [#104106](https://github.com/getsentry/sentry/pull/104106) +- fix(aci): accept str instead of int for extrapolation mode in validator by @nikkikapadia in [#104015](https://github.com/getsentry/sentry/pull/104015) +- fix(waterfall): Search for previous and next traces across environments by @Lms24 in [#104095](https://github.com/getsentry/sentry/pull/104095) +- fix(playstation): Remove option to not fetch prospero dumps from tempest by @mujacica in [#104042](https://github.com/getsentry/sentry/pull/104042) +- fix(waterfall): Relax "next_trace" lookup to just the trace id by @Lms24 in [#104047](https://github.com/getsentry/sentry/pull/104047) +- fix(dashboards): Updates dashboard quota to not count prebuilt dashboards by @edwardgou-sentry in [#103985](https://github.com/getsentry/sentry/pull/103985) +- fix(billing): Check self serve partner value in hasBillingInfo by @brendanhsentry in [#104066](https://github.com/getsentry/sentry/pull/104066) +- fix(Replay): Include Timestamp Attribute in Trace Item by @cliffordxing in [#104030](https://github.com/getsentry/sentry/pull/104030) +- fix(aci): use aggregate output type for % change detection charts by @scttcper in [#104061](https://github.com/getsentry/sentry/pull/104061) +- fix(ACI): Fix adding sentry app action to a workflow by @ceorourke in [#103790](https://github.com/getsentry/sentry/pull/103790) +- fix(aci): handle fake alert rule ids in AlertRuleDetector lookup by @ameliahsu in [#104031](https://github.com/getsentry/sentry/pull/104031) +- fix(aci): Disable highlighting series on hover by @scttcper in [#103950](https://github.com/getsentry/sentry/pull/103950) +- fix(billing): Let backend handle dynamic validation by @isabellaenriquez in [#103977](https://github.com/getsentry/sentry/pull/103977) +- fix(ui): Fix typo in Laravel onboarding text by @romeopopescu in [#103896](https://github.com/getsentry/sentry/pull/103896) +- fix(settings): Prevent infinite requests in forward stats by @scttcper in [#104032](https://github.com/getsentry/sentry/pull/104032) +- fix(aci): don't fire action if there is a conflict when creating WAGS by @cathteng in [#104002](https://github.com/getsentry/sentry/pull/104002) +- fix(aci): Multiply release data values by 100 by @scttcper in [#103981](https://github.com/getsentry/sentry/pull/103981) +- fix(db): Avoid project db query by accessing just its id by @beezz in [#104008](https://github.com/getsentry/sentry/pull/104008) +- fix(ourlogs): Add drawer QP and fix scroll by @k-fish in [#103979](https://github.com/getsentry/sentry/pull/103979) +- fix(monitors): Allow editing cron monitor detectors with existing slug by @evanpurkhiser in [#103846](https://github.com/getsentry/sentry/pull/103846) +- fix(tracemetrics): Wait for datascanned by @k-fish in [#103993](https://github.com/getsentry/sentry/pull/103993) +- fix(preprod): fix positioning of app size tooltip by @trevor-e in [#103978](https://github.com/getsentry/sentry/pull/103978) +- fix(misc): Fix getExactDuration behaviour when given a precision parameter by @kenzoengineer in [#103958](https://github.com/getsentry/sentry/pull/103958) +- fix(aci): support notifications for non-dual written metric detectors by @mifu67 in [#103938](https://github.com/getsentry/sentry/pull/103938) +- fix(aci): correctly query open periods within date range by @ameliahsu in [#103924](https://github.com/getsentry/sentry/pull/103924) +- fix(tracemetrics): Fix analytics firing by @k-fish in [#103982](https://github.com/getsentry/sentry/pull/103982) +- fix(ACI): Migrate fallthroughType to fallthrough_type by @ceorourke in [#103764](https://github.com/getsentry/sentry/pull/103764) +- fix(performance): Fixes web vital issue trace links not working sometimes by @edwardgou-sentry in [#103859](https://github.com/getsentry/sentry/pull/103859) +- fix(billing): Handle emirates by @isabellaenriquez in [#103973](https://github.com/getsentry/sentry/pull/103973) +- fix: Code signature error list missing newlines by @noahsmartin in [#103969](https://github.com/getsentry/sentry/pull/103969) +- fix(profiling): Link event id for profiles in trace view by @Zylphrex in [#103921](https://github.com/getsentry/sentry/pull/103921) +- fix(aci): Try our best in Project.transfer_to by @kcons in [#103704](https://github.com/getsentry/sentry/pull/103704) +- fix(issues): resolve column styling problems in top issues by @cvxluo in [#103946](https://github.com/getsentry/sentry/pull/103946) +- fix(aci): always combine rule and workflow fire history in API by @cathteng in [#103945](https://github.com/getsentry/sentry/pull/103945) +- fix(Replay): Update Timestamp Start Query by @cliffordxing in [#103942](https://github.com/getsentry/sentry/pull/103942) +- fix(issues): Fix issue search single page count inaccuracy by @yuvmen in [#103865](https://github.com/getsentry/sentry/pull/103865) +- fix(aci): simplify DetectorWorkflow connection permission requirements by @ameliahsu in [#103799](https://github.com/getsentry/sentry/pull/103799) +- fix(alerts): filter out orphaned metric alerts when checking limit by @cathteng in [#103912](https://github.com/getsentry/sentry/pull/103912) +- fix(mcp): Use mutable search in mcp link generation by @Zylphrex in [#103911](https://github.com/getsentry/sentry/pull/103911) +- fix(ACI): Accept fallthrough type in actions by @ceorourke in [#103694](https://github.com/getsentry/sentry/pull/103694) +- fix(aci): prevent deletion of system-created monitors in API by @ameliahsu in [#103843](https://github.com/getsentry/sentry/pull/103843) +- fix(deletions): Fix MonitorCheckIn direct deletion by @yuvmen in [#103907](https://github.com/getsentry/sentry/pull/103907) +- fix(aci): Immediate deletion of workflow-detector connections in DELETE endpoints by @malwilley in [#103869](https://github.com/getsentry/sentry/pull/103869) +- fix(aci): Snake casing for data sources in evidence data by @malwilley in [#103858](https://github.com/getsentry/sentry/pull/103858) +- fix(aci): add back logic to create IGOP associations for long-standing incidents by @mifu67 in [#103778](https://github.com/getsentry/sentry/pull/103778) +- fix(explorer): set correct thread id in metadata by @roaga in [#103876](https://github.com/getsentry/sentry/pull/103876) +- fix(tracemetrics): Add metric button not working by @Zylphrex in [#103900](https://github.com/getsentry/sentry/pull/103900) +- fix(ui): point hardcoded dark background colors to UI2 theme token values by @TkDodo in [#103886](https://github.com/getsentry/sentry/pull/103886) +- fix(ui2): align OperationDot by @TkDodo in [#103819](https://github.com/getsentry/sentry/pull/103819) +- fix(explorer): accept trace id for flamegraph tool by @roaga in [#103818](https://github.com/getsentry/sentry/pull/103818) +- fix(eap): Update attribute key name provided in `DeleteTraceItemsRequest` RPCs by @shashjar in [#103867](https://github.com/getsentry/sentry/pull/103867) +- fix(eventstream): Fix trace item inserts to EAP via Snuba HTTP backend by @shashjar in [#103857](https://github.com/getsentry/sentry/pull/103857) +- fix(aci): prevent deletion of system-created monitors in UI by @ameliahsu in [#103838](https://github.com/getsentry/sentry/pull/103838) +- fix(aci): json.dump sentry app settings value fields by @cathteng in [#103845](https://github.com/getsentry/sentry/pull/103845) +- fix(eap): Update the filter provided when issuing `DeleteTraceItemsRequest` RPCs by @shashjar in [#103848](https://github.com/getsentry/sentry/pull/103848) +- fix(migrations): Fail migrations that delete models if no historical_silo_assignment is found by @wedamija in [#103702](https://github.com/getsentry/sentry/pull/103702) +- fix(ui): Remove span wrapping avatar near commit author by @evanpurkhiser in [#103824](https://github.com/getsentry/sentry/pull/103824) +- fix(aci): Retry failed Seer anomaly data fetches by @kcons in [#103788](https://github.com/getsentry/sentry/pull/103788) +- fix(deletion): Fix MonitorCheckIn deletion failures by @yuvmen in [#103786](https://github.com/getsentry/sentry/pull/103786) +- fix(aci): Hide failure rate and hide string options for p50 by @scttcper in [#103697](https://github.com/getsentry/sentry/pull/103697) +- fix(eap): Pass `trace_item_type` in `RequestMeta` for `DeleteTraceItemsRequest` RPC by @shashjar in [#103784](https://github.com/getsentry/sentry/pull/103784) +- fix(preprod): update frontend to new missing_dsym_binaries field by @trevor-e in [#103735](https://github.com/getsentry/sentry/pull/103735) +- fix(eap): Provide correct endpoint name when deleting trace items from EAP by @shashjar in [#103749](https://github.com/getsentry/sentry/pull/103749) +- fix(aci): remove email targetDisplay from UI POST request by @ameliahsu in [#103679](https://github.com/getsentry/sentry/pull/103679) +- fix(integrations): update usage of is_enabled data forwarding by @liuirene256 in [#103584](https://github.com/getsentry/sentry/pull/103584) +- fix(slack): Truncate blocks to be less than 50 for digest notifications by @Christinarlong in [#103498](https://github.com/getsentry/sentry/pull/103498) +- fix(tracemetrics): Add 'new query' for save-as behaviour by @k-fish in [#103734](https://github.com/getsentry/sentry/pull/103734) +- fix(explorer): new copy for metric attrs tool by @roaga in [#103728](https://github.com/getsentry/sentry/pull/103728) +- fix(replay): Adjust placeholder height in user badge component by @jerryzhou196 in [#103726](https://github.com/getsentry/sentry/pull/103726) +- fix(replay): Fix infinite re-render in breadcrumbs by @billyvg in [#103373](https://github.com/getsentry/sentry/pull/103373) +- fix(explore): fix issue with not being able to save explore queries with a start and end time stamp selected by @edwardgou-sentry in [#103725](https://github.com/getsentry/sentry/pull/103725) +- fix(explorer): better profile thread selection logic by @roaga in [#103656](https://github.com/getsentry/sentry/pull/103656) +- fix(explorer): stricter short ID regex by @roaga in [#103721](https://github.com/getsentry/sentry/pull/103721) +- fix(tracemetrics): Validate aggregate sort bys for metrics by @Zylphrex in [#103555](https://github.com/getsentry/sentry/pull/103555) +- fix(preprod): Deduplicate sibling artifacts by app_id to prevent duplicate rows in status checks (EME-610) by @runningcode in [#103528](https://github.com/getsentry/sentry/pull/103528) +- fix(encryption): Improve log message when we load 0 keys by @vgrozdanic in [#103715](https://github.com/getsentry/sentry/pull/103715) +- fix(eu_data_export): fixes sts transfer job schedule by @viglia in [#103712](https://github.com/getsentry/sentry/pull/103712) +- fix(aci): filter out non-alertable and broken sentry apps in available actions endpoint by @ameliahsu in [#101866](https://github.com/getsentry/sentry/pull/101866) +- fix(timeseries): Handle orderbys not in groupby by @wmak in [#103547](https://github.com/getsentry/sentry/pull/103547) +- fix(checkout): Use correct unit for attachments by @isabellaenriquez in [#103647](https://github.com/getsentry/sentry/pull/103647) +- fix(aci): Filter connected monitors drawer by project by @malwilley in [#103572](https://github.com/getsentry/sentry/pull/103572) +- fix(aci): close IGOP relationships in more cases and heal broken relationships by @mifu67 in [#103407](https://github.com/getsentry/sentry/pull/103407) +- fix(ourlogs): Add error event to log list by @k-fish in [#103649](https://github.com/getsentry/sentry/pull/103649) +- fix(dashboards): Fix incorrect URL construction for "Open in Explore" for Logs widgets by @gggritso in [#103642](https://github.com/getsentry/sentry/pull/103642) +- fix(issues): Remove sentry logging on invalid url by @scttcper in [#103574](https://github.com/getsentry/sentry/pull/103574) +- fix(feedback): remove feature badge for AI summaries & categories by @srest2021 in [#103571](https://github.com/getsentry/sentry/pull/103571) +- fix(detectors): Navigate to detector type list page after deletion by @evanpurkhiser in [#103562](https://github.com/getsentry/sentry/pull/103562) +- fix(aci): Apply chart zoom on automation history by @scttcper in [#103392](https://github.com/getsentry/sentry/pull/103392) +- fix(aci): Autosize description, remove padding by @scttcper in [#103546](https://github.com/getsentry/sentry/pull/103546) +- fix(issues): Attribute error in issue details thrown on None tag values by @yuvmen in [#103500](https://github.com/getsentry/sentry/pull/103500) +- fix(replay): fix buttons from being cutoff by @jerryzhou196 in [#103522](https://github.com/getsentry/sentry/pull/103522) +- fix(deletions): Fix Monitor deletion timeouts by @yuvmen in [#103495](https://github.com/getsentry/sentry/pull/103495) +- fix(checkout): Insights on Team by @isabellaenriquez in [#103514](https://github.com/getsentry/sentry/pull/103514) +- fix(replay): move hover to be over buttons by @jerryzhou196 in [#103524](https://github.com/getsentry/sentry/pull/103524) +- fix(aci): update hits count for workflow fire history by @ameliahsu in [#103400](https://github.com/getsentry/sentry/pull/103400) +- fix(crons): Add trailing slash to monitor environment detail endpoints by @evanpurkhiser in [#103480](https://github.com/getsentry/sentry/pull/103480) +- fix(replay): validate replay start/end for summaries by @michellewzhang in [#103388](https://github.com/getsentry/sentry/pull/103388) +- fix(issues): use correct tz for absolute date picker by @cvxluo in [#103423](https://github.com/getsentry/sentry/pull/103423) +- fix(timeseries): Remove unwanted `groupBy` parameter by @gggritso in [#103482](https://github.com/getsentry/sentry/pull/103482) +- fix(tracemetrics): Don't show metrics on error by @k-fish in [#103516](https://github.com/getsentry/sentry/pull/103516) +- fix(seer): Increase lock duration for issue summary generation by @seer-by-sentry in [#103477](https://github.com/getsentry/sentry/pull/103477) +- fix(grouping): Ensure custom titles use the correct frame by @lobsterkatie in [#103425](https://github.com/getsentry/sentry/pull/103425) +- fix(loader): respect `prefers-reduced-motion` by @natemoo-re in [#103461](https://github.com/getsentry/sentry/pull/103461) +- fix(integrations): data forwarding bug fixes by @liuirene256 in [#103393](https://github.com/getsentry/sentry/pull/103393) +- fix(ui): Do not add extranious height to AI insights onboarding by @evanpurkhiser in [#103451](https://github.com/getsentry/sentry/pull/103451) +- fix(dashboards): set widget to loading before adding to queue by @DominikB2014 in [#103449](https://github.com/getsentry/sentry/pull/103449) +- fix(explorer): set thread id in profile navigation by @roaga in [#103427](https://github.com/getsentry/sentry/pull/103427) +- fix(explorer): fix profile thread selection by @roaga in [#103426](https://github.com/getsentry/sentry/pull/103426) +- fix(dashboards): queue doesn't apply to all datasets by @DominikB2014 in [#103444](https://github.com/getsentry/sentry/pull/103444) +- fix(search): Fix handle backslashes in wildcard operators by @Zylphrex in [#103379](https://github.com/getsentry/sentry/pull/103379) +- fix(explorer): only return active projects by @roaga in [#103434](https://github.com/getsentry/sentry/pull/103434) +- fix(spans): Shim more fields for issue detectors by @jjbayer in [#103353](https://github.com/getsentry/sentry/pull/103353) + +### Build / dependencies / internal 🔧 + +- ref(eap): Remove AnyResolved type by @Zylphrex in [#104130](https://github.com/getsentry/sentry/pull/104130) +- chore(explore): Remove used explore flag backend by @Zylphrex in [#104021](https://github.com/getsentry/sentry/pull/104021) +- chore(explore): Add in cross event query param by @nsdeschenes in [#103666](https://github.com/getsentry/sentry/pull/103666) +- ref(explorer): add copy and nav for errors search by @aliu39 in [#104078](https://github.com/getsentry/sentry/pull/104078) +- ref(explorer): update log tool copy by @aliu39 in [#104065](https://github.com/getsentry/sentry/pull/104065) +- chore(aci): bump offset for fake IDs by @mifu67 in [#104051](https://github.com/getsentry/sentry/pull/104051) +- chore: Remove vercel logs feature flag from backend by @AbhiPrasad in [#103975](https://github.com/getsentry/sentry/pull/103975) +- ref(issues): move top issues tab to top by @cvxluo in [#104056](https://github.com/getsentry/sentry/pull/104056) +- chore(tests): skip flaky test by @mifu67 in [#104057](https://github.com/getsentry/sentry/pull/104057) +- chore(logs): Remove ourlogs-high-fidelity feature flag frontend by @Zylphrex in [#104016](https://github.com/getsentry/sentry/pull/104016) +- chore(dashboards): Address global filter UI feedback by @Ahmed-Labs in [#103902](https://github.com/getsentry/sentry/pull/103902) +- chore(explore): Remove visibility-explore-aggregate-editor flag frontend by @Zylphrex in [#104020](https://github.com/getsentry/sentry/pull/104020) +- chore(logs): Remove ourlogs-high-fidelity feature flag backend by @Zylphrex in [#104017](https://github.com/getsentry/sentry/pull/104017) +- ref(issues): delete top issue breadcrumbs by @cvxluo in [#104028](https://github.com/getsentry/sentry/pull/104028) +- chore(feedback): Prefer over useFeedbackForm() by @ryan953 in [#103916](https://github.com/getsentry/sentry/pull/103916) +- chore(seer): Setup new, blank, page for Seer settings to live by @ryan953 in [#103990](https://github.com/getsentry/sentry/pull/103990) +- ref(explorer): rm old issue rpc by @aliu39 in [#103935](https://github.com/getsentry/sentry/pull/103935) +- ref(ui): Use type-fest type for flatten by @scttcper in [#103998](https://github.com/getsentry/sentry/pull/103998) +- chore(preprod): make build details and build compare breadcrumb consistent (EME-657, EME-659) by @mtopo27 in [#103994](https://github.com/getsentry/sentry/pull/103994) +- chore(traces): Register flag for cross event querying by @nsdeschenes in [#103987](https://github.com/getsentry/sentry/pull/103987) +- ref(events): Singularize pluralized params in the events endpoint by @wmak in [#103923](https://github.com/getsentry/sentry/pull/103923) +- chore(deletions): Change schedule & date ranges by @armenzg in [#103897](https://github.com/getsentry/sentry/pull/103897) +- chore(releases): fix releases tooltip width by @mtopo27 in [#103955](https://github.com/getsentry/sentry/pull/103955) +- chore(explore): Hard code sdk name and version as span string attrs by @Zylphrex in [#103980](https://github.com/getsentry/sentry/pull/103980) +- ref: switch to type-fest for utility types by @TkDodo in [#103961](https://github.com/getsentry/sentry/pull/103961) +- chore(preprod): binary export insight by @mtopo27 in [#102459](https://github.com/getsentry/sentry/pull/102459) +- ref(redis): Do not use redis-blaster for snowflake IDs generation by @beezz in [#103967](https://github.com/getsentry/sentry/pull/103967) +- chore(preprod): fix typo in install modal by @mtopo27 in [#103968](https://github.com/getsentry/sentry/pull/103968) +- ref(issues): clean up top issues styles by @cvxluo in [#103944](https://github.com/getsentry/sentry/pull/103944) +- chore(aci): log missing WorkflowActionGroupStatus after bulk create by @cathteng in [#103928](https://github.com/getsentry/sentry/pull/103928) +- chore(aci): allow feedback issues through workflow engine by @cathteng in [#100153](https://github.com/getsentry/sentry/pull/100153) +- ref(issues): use top issues endpoint by @cvxluo in [#103899](https://github.com/getsentry/sentry/pull/103899) +- ref(tracemetrics): Add test for metrics tab by @k-fish in [#103901](https://github.com/getsentry/sentry/pull/103901) +- chore(replay): Cleanup zendesk feedback target in replay details by @ryan953 in [#103908](https://github.com/getsentry/sentry/pull/103908) +- chore(preprod): amplitude events for emerge pages (EME-638) by @mtopo27 in [#103801](https://github.com/getsentry/sentry/pull/103801) +- chore(releases): add give feedback to release details page by @mtopo27 in [#103866](https://github.com/getsentry/sentry/pull/103866) +- chore(aci): fetch issue stream detector in process_workflows by @cathteng in [#103709](https://github.com/getsentry/sentry/pull/103709) +- chore(traces): Swap from `useFeedbackWidget` to `useFeedbackForm()` in trace viewer by @ryan953 in [#103903](https://github.com/getsentry/sentry/pull/103903) +- chore(aci): remove lastChecked from open period serializer by @mifu67 in [#103868](https://github.com/getsentry/sentry/pull/103868) +- chore(billing): Add data-test-id for acceptance tests by @isabellaenriquez in [#103898](https://github.com/getsentry/sentry/pull/103898) +- chore: Refactor FeedbackButton to accept all the feedback options by @ryan953 in [#103794](https://github.com/getsentry/sentry/pull/103794) +- ref: set checkJs globally to true by @TkDodo in [#103890](https://github.com/getsentry/sentry/pull/103890) +- ref(aci): remove detector usage in WorkflowFireHistory and action firing by @cathteng in [#103082](https://github.com/getsentry/sentry/pull/103082) +- ref(taskbroker): Clarify docstring & class name by @armenzg in [#103893](https://github.com/getsentry/sentry/pull/103893) +- ref(explorer): rpc to support issue/event detail queries with event id only by @aliu39 in [#103787](https://github.com/getsentry/sentry/pull/103787) +- ref: bump Granian to 2.6, enable pname extra by @gi0baro in [#103817](https://github.com/getsentry/sentry/pull/103817) +- ref(deletions): Use the same logic to schedule tasks by @armenzg in [#103836](https://github.com/getsentry/sentry/pull/103836) +- ci: Enable Objectstore by @lcian in [#103483](https://github.com/getsentry/sentry/pull/103483) +- chore(eco): allow deleting disabled org integrations by @cathteng in [#103785](https://github.com/getsentry/sentry/pull/103785) +- chore(releases): remove floating feedback widget in favor of header feedback (ENG-5961) by @mtopo27 in [#103862](https://github.com/getsentry/sentry/pull/103862) +- ref(billing): Convert CreditType to dynamic type union by @dashed in [#103815](https://github.com/getsentry/sentry/pull/103815) +- ref(ui): Refactor EmptyMessage to use core components and simplify API by @evanpurkhiser in [#103798](https://github.com/getsentry/sentry/pull/103798) +- chore(ACI): Pop actions off of conditions by @ceorourke in [#103755](https://github.com/getsentry/sentry/pull/103755) +- ref(releases): Migrate release actions off deprecated route props by @scttcper in [#103705](https://github.com/getsentry/sentry/pull/103705) +- ref(detectors): Hide environment selector for cron monitors by @evanpurkhiser in [#103841](https://github.com/getsentry/sentry/pull/103841) +- chore(replay): Remove an extra wrapper in the Replay List page header by @ryan953 in [#103847](https://github.com/getsentry/sentry/pull/103847) +- ref(aci): decouple detector from workflowfirehistory in API by @cathteng in [#102918](https://github.com/getsentry/sentry/pull/102918) +- ref(explorer): support start/end for table and timeseries rpcs by @aliu39 in [#103779](https://github.com/getsentry/sentry/pull/103779) +- ref(explore): Increase trace explorer visualization limit from 4 to 8 by @JoshFerge in [#103769](https://github.com/getsentry/sentry/pull/103769) +- ref(seer): Update add-on enum by @isabellaenriquez in [#103695](https://github.com/getsentry/sentry/pull/103695) +- ref(explorer): add copy for log details tool by @aliu39 in [#103765](https://github.com/getsentry/sentry/pull/103765) +- ref(billing): Convert InvoiceItemType to dynamic type union by @dashed in [#103664](https://github.com/getsentry/sentry/pull/103664) +- ref(seer): Introduce legacy add-on by @isabellaenriquez in [#103724](https://github.com/getsentry/sentry/pull/103724) +- ref(compactSelect): improve clearing values by @TkDodo in [#103720](https://github.com/getsentry/sentry/pull/103720) +- ref: short circuit post_upgrade hook for migrations-drift by @joshuarli in [#103337](https://github.com/getsentry/sentry/pull/103337) +- ref(eap): Create a constant for the EAP insert items endpoint path by @shashjar in [#103780](https://github.com/getsentry/sentry/pull/103780) +- ref(billing): Remove QUOTA_PREVENT_USERS notification by @brendanhsentry in [#103587](https://github.com/getsentry/sentry/pull/103587) +- chore(replay): add new feature flag for new UI by @jerryzhou196 in [#103762](https://github.com/getsentry/sentry/pull/103762) +- ref: bump sentry-relay to 0.9.22 by @srest2021 in [#103736](https://github.com/getsentry/sentry/pull/103736) +- ref(feedback): move feedback empty state to /feedback by @michellewzhang in [#103763](https://github.com/getsentry/sentry/pull/103763) +- ref(preprod): convert missing_dsym_binaries to bool by @trevor-e in [#103733](https://github.com/getsentry/sentry/pull/103733) +- ref(crons): Simplify is_muted logic using all() instead of double negative by @evanpurkhiser in [#103732](https://github.com/getsentry/sentry/pull/103732) +- chore(integrations): make project_ids not required by @liuirene256 in [#103747](https://github.com/getsentry/sentry/pull/103747) +- chore(insights): Remove beta badge from Web Vitals seer suggestions by @edwardgou-sentry in [#103722](https://github.com/getsentry/sentry/pull/103722) +- chore(issues): Remove old reference to performance category when building alert message by @malwilley in [#103580](https://github.com/getsentry/sentry/pull/103580) +- chore(migrations): Fix up failed deletes by @wedamija in [#103703](https://github.com/getsentry/sentry/pull/103703) +- ref(admin): Migrate ChangeDatesModal away from deprecated form by @scttcper in [#103499](https://github.com/getsentry/sentry/pull/103499) +- ref(crons): Fix N+1 query in MonitorSerializer by removing is_muted property by @evanpurkhiser in [#103693](https://github.com/getsentry/sentry/pull/103693) +- ref(crons): Delete Monitor.is_muted database column (stage 4) by @evanpurkhiser in [#103677](https://github.com/getsentry/sentry/pull/103677) +- ref(aci): remove passing in detector to action.trigger attempt 2 by @cathteng in [#103099](https://github.com/getsentry/sentry/pull/103099) +- chore(aci): create DetectorGroup with null detector if we can't find event by @cathteng in [#103700](https://github.com/getsentry/sentry/pull/103700) +- ref: compositeSelect gets a required trigger by @TkDodo in [#103713](https://github.com/getsentry/sentry/pull/103713) +- ref(checkout): Update route by @isabellaenriquez in [#103665](https://github.com/getsentry/sentry/pull/103665) +- ref(eu_data_export): return list of jobs by @viglia in [#103717](https://github.com/getsentry/sentry/pull/103717) +- chore(preprod): Use nonblocked project_id tag for other e2e metric by @NicoHinderling in [#103710](https://github.com/getsentry/sentry/pull/103710) +- chore(deletions): Drop `sentry_incidentseen` and `sentry_incidentsubscription` leftover tables by @yuvmen in [#103559](https://github.com/getsentry/sentry/pull/103559) +- ref(crons): Invalidate monitor query after environment muting by @evanpurkhiser in [#103692](https://github.com/getsentry/sentry/pull/103692) +- ref(crons): Allow unmuting monitor envs when monitor "muted" by @evanpurkhiser in [#103689](https://github.com/getsentry/sentry/pull/103689) +- ref(crons): Disable mute button when monitor has no environments (stage 5) by @evanpurkhiser in [#103568](https://github.com/getsentry/sentry/pull/103568) +- chore(aci): Actually delete WorkflowFireHistory.is_single_written by @kcons in [#103683](https://github.com/getsentry/sentry/pull/103683) +- ref(stats): Remove estimation logic for continuous profiling by @brendanhsentry in [#103658](https://github.com/getsentry/sentry/pull/103658) +- ref(admin): Migrate change contract away from deprecated form by @scttcper in [#103503](https://github.com/getsentry/sentry/pull/103503) +- ref(admin): Migrate end immediate action from deprecated forms by @scttcper in [#103501](https://github.com/getsentry/sentry/pull/103501) +- chore(eco): default comment bots to off by @cathteng in [#103525](https://github.com/getsentry/sentry/pull/103525) +- chore(insights): Removes free seer runs on Web Vitals issues by @edwardgou-sentry in [#103675](https://github.com/getsentry/sentry/pull/103675) +- ref(dashboards): Remove spread from tags reduce by @scttcper in [#103670](https://github.com/getsentry/sentry/pull/103670) +- ref(crons): Remove Monitor.is_muted field, make it computed (stage 3) by @evanpurkhiser in [#103567](https://github.com/getsentry/sentry/pull/103567) +- chore(aci): update product links for LA by @ameliahsu in [#103671](https://github.com/getsentry/sentry/pull/103671) +- chore(checkout): Add path pattern by @isabellaenriquez in [#103668](https://github.com/getsentry/sentry/pull/103668) +- chore(aci): Remove WorkflowFireHistory.is_single_written by @kcons in [#103502](https://github.com/getsentry/sentry/pull/103502) +- ref(scraps): make value and onChange required on compactSelect by @TkDodo in [#103654](https://github.com/getsentry/sentry/pull/103654) +- ref: type reduce by @TkDodo in [#103631](https://github.com/getsentry/sentry/pull/103631) +- ref(workflow_engine): Further reduce debug log size by @saponifi3d in [#103505](https://github.com/getsentry/sentry/pull/103505) +- chore(issues): Remove reference to performance category in group has_replay by @malwilley in [#103585](https://github.com/getsentry/sentry/pull/103585) +- chore: Tag extrapolation mode by @shruthilayaj in [#103540](https://github.com/getsentry/sentry/pull/103540) +- ref: do not allow uncontrolled compactSelect by @TkDodo in [#103637](https://github.com/getsentry/sentry/pull/103637) +- chore(explorer): add more debug logs for profiling by @roaga in [#103640](https://github.com/getsentry/sentry/pull/103640) +- chore(ACI): Clean up metric issue by @ceorourke in [#103569](https://github.com/getsentry/sentry/pull/103569) +- chore(Replay): Update EAP Flag Condition by @cliffordxing in [#103556](https://github.com/getsentry/sentry/pull/103556) +- ref(reprocessing): Update documentation for reprocessing flow by @shashjar in [#102961](https://github.com/getsentry/sentry/pull/102961) +- ref(ui): Use Stack over custom FormStack by @evanpurkhiser in [#103558](https://github.com/getsentry/sentry/pull/103558) +- ref(crons): Add migration to backfill MonitorEnvironment.is_muted by @evanpurkhiser in [#103324](https://github.com/getsentry/sentry/pull/103324) +- chore: Update API owners for Data Browsing by @gggritso in [#103542](https://github.com/getsentry/sentry/pull/103542) +- ref(crons): Show processing errors on cron detectors list by @evanpurkhiser in [#103527](https://github.com/getsentry/sentry/pull/103527) +- ref(insights): Refactor Seer Analysis sidebar component in the Web Vitals page overview by @edwardgou-sentry in [#103459](https://github.com/getsentry/sentry/pull/103459) +- ref: bump sentry-relay to 0.9.21 by @brendanhsentry in [#103474](https://github.com/getsentry/sentry/pull/103474) +- ref(performance): Improve web vitals issue check using occurrence type instead by @edwardgou-sentry in [#103471](https://github.com/getsentry/sentry/pull/103471) +- chore(codeowners): adds data-browsing as owner by @alexjillard in [#102998](https://github.com/getsentry/sentry/pull/102998) +- ref(replay): filter out events before replay start for log messages by @michellewzhang in [#102931](https://github.com/getsentry/sentry/pull/102931) +- chore(aci): backfill detectorgroup for metric issues by @cathteng in [#103399](https://github.com/getsentry/sentry/pull/103399) +- ref: cleanup intent-preloading feature flag by @TkDodo in [#103517](https://github.com/getsentry/sentry/pull/103517) +- ref: bump objectstore-client to 0.0.11 by @getsentry-bot in [#103510](https://github.com/getsentry/sentry/pull/103510) +- chore: disable flakey test by @JoshFerge in [#103497](https://github.com/getsentry/sentry/pull/103497) +- ref(explorer): clean up old trace rpcs and auto-select sort field by @aliu39 in [#103491](https://github.com/getsentry/sentry/pull/103491) +- ref(explorer): useSessionStorage to persist current run id by @aliu39 in [#103467](https://github.com/getsentry/sentry/pull/103467) +- chore(explorer): add logs to profile tool by @roaga in [#103479](https://github.com/getsentry/sentry/pull/103479) +- ref(conduit): Adjust permissions for conduit demo by @IanWoodard in [#103475](https://github.com/getsentry/sentry/pull/103475) +- chore(preprod): abstract metric cards for build comparison by @mtopo27 in [#103469](https://github.com/getsentry/sentry/pull/103469) +- ref(autofix): Reuse existing prism language map by @scttcper in [#103476](https://github.com/getsentry/sentry/pull/103476) +- chore(preprod): fix casing by @mtopo27 in [#103472](https://github.com/getsentry/sentry/pull/103472) +- ref(issues): Clarify markdown copy button label and show line count by @evanpurkhiser in [#103448](https://github.com/getsentry/sentry/pull/103448) +- ref(workflow_engine): Make logs not insanely expensive by @saponifi3d in [#103464](https://github.com/getsentry/sentry/pull/103464) +- ref(seer): Rename billing flag by @isabellaenriquez in [#103447](https://github.com/getsentry/sentry/pull/103447) +- ref(checkout): Cleanup file structure by @isabellaenriquez in [#103378](https://github.com/getsentry/sentry/pull/103378) +- ref(objectstore): Upgrade client to 0.0.10 by @lcian in [#103365](https://github.com/getsentry/sentry/pull/103365) +- ref(profiling): remove flamegraph chunked query strategy feature flag by @viglia in [#103284](https://github.com/getsentry/sentry/pull/103284) + +### Documentation 📚 + +- docs: Add update-migration script to AGENTS.md by @kcons in [#103770](https://github.com/getsentry/sentry/pull/103770) + +### Other + +- typing: Remove organization_events_meta from soft list by @armenzg in [#104102](https://github.com/getsentry/sentry/pull/104102) +- feat(explore-attr-breakdowns): Removing hovering CTA by @Abdkhan14 in [#104144](https://github.com/getsentry/sentry/pull/104144) +- Revert setting extrapolation mode to sample weighted by @shruthilayaj in [#104136](https://github.com/getsentry/sentry/pull/104136) +- ref(redis) Don't use redis-blaster for snowflake ids v3 by @markstory in [#104114](https://github.com/getsentry/sentry/pull/104114) +- fix(mcp-onboarding): Support fullstack JS platforms by @ArthurKnaus in [#104153](https://github.com/getsentry/sentry/pull/104153) +- add copy as markdown to user feedback by @mtopo27 in [#103954](https://github.com/getsentry/sentry/pull/103954) +- ref(✂️): update knip and address new findings by @TkDodo in [#104151](https://github.com/getsentry/sentry/pull/104151) +- flags(size): Add new flag to gate size issue reporting by @chromy in [#104122](https://github.com/getsentry/sentry/pull/104122) +- ref(dynamic-sampling): simplify bias combination logic by @shellmayr in [#104119](https://github.com/getsentry/sentry/pull/104119) +- deps(js): Upgrade sentry js sdk deps to `10.27.0` by @AbhiPrasad in [#104110](https://github.com/getsentry/sentry/pull/104110) +- fix(date-page-filter): Fallback to max pickable days when absolute ra… by @Zylphrex in [#104111](https://github.com/getsentry/sentry/pull/104111) +- feat(cells) Add date_updated to organizationmapping v2 by @markstory in [#103915](https://github.com/getsentry/sentry/pull/103915) +- Icon SlashForward Replacing Chevron in Breadcrumbs by @Jesse-Box in [#104120](https://github.com/getsentry/sentry/pull/104120) +- typing(tests): Minor changes by @armenzg in [#104105](https://github.com/getsentry/sentry/pull/104105) +- feat(explore-attr-breakdowns): Adding error and empty search state UI by @Abdkhan14 in [#104012](https://github.com/getsentry/sentry/pull/104012) +- feature(explore-attr-breakdowns): Removing yAxis heuristic from selection hint by @Abdkhan14 in [#104087](https://github.com/getsentry/sentry/pull/104087) +- fix(uptime) Add a missing task import for uptime by @markstory in [#104104](https://github.com/getsentry/sentry/pull/104104) +- chore(search-query-builder): Add metric for invalids and warnings by @Zylphrex in [#104076](https://github.com/getsentry/sentry/pull/104076) +- chore(hybridcloud) Align keys used for organization snowflakeids by @markstory in [#104075](https://github.com/getsentry/sentry/pull/104075) +- feat(cells) Add silo annotations to email, debug, and plugin views by @markstory in [#104034](https://github.com/getsentry/sentry/pull/104034) +- ref(llm-detection): Send enhanced span data to support improved Seer analysis by @nora-shap in [#103871](https://github.com/getsentry/sentry/pull/103871) +- chore(llm-detector): Add GenAI Consent Check by @roggenkemper in [#104060](https://github.com/getsentry/sentry/pull/104060) +- feat(cursor-agent): include email and key name by @jennmueng in [#103989](https://github.com/getsentry/sentry/pull/103989) +- fix(cursor-agent): ui handling of when you have more than 1 integration by @jennmueng in [#104007](https://github.com/getsentry/sentry/pull/104007) + +_Plus 134 more_ + 25.11.0 ------- diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 126be8e952b22c..a4588247cc2c42 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -31,7 +31,7 @@ releases: 0004_cleanup_failed_safe_deletes replays: 0006_add_bulk_delete_job -sentry: 1009_add_date_updated_to_organizationmapping +sentry: 1010_add_organizationcontributors_table social_auth: 0003_social_auth_json_field diff --git a/package.json b/package.json index 2b3aff608fec4c..027e3c08fed422 100644 --- a/package.json +++ b/package.json @@ -68,13 +68,13 @@ "@sentry-internal/rrweb": "2.40.0", "@sentry-internal/rrweb-player": "2.40.0", "@sentry-internal/rrweb-snapshot": "2.40.0", - "@sentry/core": "10.23.0", - "@sentry/node": "10.23.0", - "@sentry/react": "10.23.0", + "@sentry/core": "10.27.0", + "@sentry/node": "10.27.0", + "@sentry/react": "10.27.0", "@sentry/release-parser": "^1.3.1", "@sentry/status-page-list": "^0.6.1", "@sentry/toolbar": "1.0.0-beta.16", - "@sentry/webpack-plugin": "4.4.0", + "@sentry/webpack-plugin": "4.6.1", "@stripe/react-stripe-js": "^3.9.2", "@stripe/stripe-js": "^5.10.0", "@swc/plugin-emotion": "11.0.3", @@ -190,7 +190,7 @@ "@prettier/plugin-oxc": "0.0.4", "@sentry-internal/rrweb-types": "2.40.0", "@sentry/jest-environment": "6.1.0", - "@sentry/profiling-node": "10.23.0", + "@sentry/profiling-node": "10.27.0", "@styled/typescript-styled-plugin": "^1.0.1", "@tanstack/eslint-plugin-query": "5.83.1", "@testing-library/dom": "10.4.0", @@ -223,7 +223,7 @@ "jest-environment-jsdom": "30.0.4", "jest-fail-on-console": "3.3.1", "jest-junit": "16.0.0", - "knip": "5.64.0", + "knip": "5.71.0", "postcss-styled-syntax": "0.7.0", "react-refresh": "0.18.0", "stylelint": "16.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 470b2c78150520..62d6fe0ee4e5f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,14 +193,14 @@ importers: specifier: 2.40.0 version: 2.40.0 '@sentry/core': - specifier: 10.23.0 - version: 10.23.0 + specifier: 10.27.0 + version: 10.27.0 '@sentry/node': - specifier: 10.23.0 - version: 10.23.0 + specifier: 10.27.0 + version: 10.27.0 '@sentry/react': - specifier: 10.23.0 - version: 10.23.0(react@19.2.0) + specifier: 10.27.0 + version: 10.27.0(react@19.2.0) '@sentry/release-parser': specifier: ^1.3.1 version: 1.3.1 @@ -211,8 +211,8 @@ importers: specifier: 1.0.0-beta.16 version: 1.0.0-beta.16(react@19.2.0) '@sentry/webpack-plugin': - specifier: 4.4.0 - version: 4.4.0(encoding@0.1.13)(webpack@5.99.6(esbuild@0.25.10)) + specifier: 4.6.1 + version: 4.6.1(encoding@0.1.13)(webpack@5.99.6(esbuild@0.25.10)) '@stripe/react-stripe-js': specifier: ^3.9.2 version: 3.9.2(@stripe/stripe-js@5.10.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -537,7 +537,7 @@ importers: version: 1.9.0(webpack-sources@3.3.3)(webpack@5.99.6(esbuild@0.25.10)) '@emotion/eslint-plugin': specifier: ^11.12.0 - version: 11.12.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@eslint/js': specifier: 9.32.0 version: 9.32.0 @@ -552,16 +552,16 @@ importers: version: 2.40.0 '@sentry/jest-environment': specifier: 6.1.0 - version: 6.1.0(@sentry/node@10.23.0)(@sentry/profiling-node@10.23.0)(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2))) + version: 6.1.0(@sentry/node@10.27.0)(@sentry/profiling-node@10.27.0)(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2))) '@sentry/profiling-node': - specifier: 10.23.0 - version: 10.23.0 + specifier: 10.27.0 + version: 10.27.0 '@styled/typescript-styled-plugin': specifier: ^1.0.1 version: 1.0.1 '@tanstack/eslint-plugin-query': specifier: 5.83.1 - version: 5.83.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 5.83.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@testing-library/dom': specifier: 10.4.0 version: 10.4.0 @@ -585,52 +585,52 @@ importers: version: 30.0.4(@babel/core@7.28.0) eslint: specifier: 9.34.0 - version: 9.34.0(jiti@2.5.1) + version: 9.34.0(jiti@2.6.1) eslint-config-prettier: specifier: 10.1.8 - version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) + version: 10.1.8(eslint@9.34.0(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^3.8.3 - version: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)) + version: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-boundaries: specifier: ^5.0.1 - version: 5.0.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)) + version: 5.0.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-import: specifier: 2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)) + version: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jest: specifier: 29.0.1 - version: 29.0.1(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))(typescript@5.9.2) + version: 29.0.1(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))(typescript@5.9.2) eslint-plugin-jest-dom: specifier: ^5.5.0 - version: 5.5.0(@testing-library/dom@10.4.0)(eslint@9.34.0(jiti@2.5.1)) + version: 5.5.0(@testing-library/dom@10.4.0)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-mdx: specifier: 3.6.2 - version: 3.6.2(eslint@9.34.0(jiti@2.5.1)) + version: 3.6.2(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-no-relative-import-paths: specifier: ^1.6.1 version: 1.6.1 eslint-plugin-react: specifier: 7.37.5 - version: 7.37.5(eslint@9.34.0(jiti@2.5.1)) + version: 7.37.5(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: 6.1.0 - version: 6.1.0(eslint@9.34.0(jiti@2.5.1)) + version: 6.1.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-react-you-might-not-need-an-effect: specifier: 0.5.3 - version: 0.5.3(eslint@9.34.0(jiti@2.5.1)) + version: 0.5.3(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-sentry: specifier: ^2.10.0 version: 2.10.0 eslint-plugin-testing-library: specifier: ^7.1.1 - version: 7.1.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 7.1.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint-plugin-typescript-sort-keys: specifier: ^3.3.0 - version: 3.3.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 3.3.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint-plugin-unicorn: specifier: ^57.0.0 - version: 57.0.0(eslint@9.34.0(jiti@2.5.1)) + version: 57.0.0(eslint@9.34.0(jiti@2.6.1)) expect-type: specifier: 1.2.1 version: 1.2.1 @@ -653,8 +653,8 @@ importers: specifier: 16.0.0 version: 16.0.0 knip: - specifier: 5.64.0 - version: 5.64.0(@types/node@22.15.21)(typescript@5.9.2) + specifier: 5.71.0 + version: 5.71.0(@types/node@22.15.21)(typescript@5.9.2) postcss-styled-syntax: specifier: 0.7.0 version: 0.7.0(postcss@8.5.3) @@ -672,7 +672,7 @@ importers: version: 5.40.0 typescript-eslint: specifier: 8.39.0 - version: 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + version: 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) optionalDependencies: fsevents: specifier: ^2.3.2 @@ -2146,8 +2146,8 @@ packages: '@napi-rs/wasm-runtime@1.0.3': resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} - '@napi-rs/wasm-runtime@1.0.5': - resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -2240,186 +2240,176 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - '@opentelemetry/api-logs@0.204.0': - resolution: {integrity: sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.57.2': - resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} - engines: {node: '>=14'} - '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.1.0': - resolution: {integrity: sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==} + '@opentelemetry/context-async-hooks@2.2.0': + resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.1.0': - resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/instrumentation-amqplib@0.51.0': - resolution: {integrity: sha512-XGmjYwjVRktD4agFnWBWQXo9SiYHKBxR6Ag3MLXwtLE4R99N3a08kGKM5SC1qOFKIELcQDGFEFT9ydXMH00Luw==} + '@opentelemetry/instrumentation-amqplib@0.55.0': + resolution: {integrity: sha512-5ULoU8p+tWcQw5PDYZn8rySptGSLZHNX/7srqo2TioPnAAcvTy6sQFQXsNPrAnyRRtYGMetXVyZUy5OaX1+IfA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-connect@0.48.0': - resolution: {integrity: sha512-OMjc3SFL4pC16PeK+tDhwP7MRvDPalYCGSvGqUhX5rASkI2H0RuxZHOWElYeXkV0WP+70Gw6JHWac/2Zqwmhdw==} + '@opentelemetry/instrumentation-connect@0.52.0': + resolution: {integrity: sha512-GXPxfNB5szMbV3I9b7kNWSmQBoBzw7MT0ui6iU/p+NIzVx3a06Ri2cdQO7tG9EKb4aKSLmfX9Cw5cKxXqX6Ohg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-dataloader@0.22.0': - resolution: {integrity: sha512-bXnTcwtngQsI1CvodFkTemrrRSQjAjZxqHVc+CJZTDnidT0T6wt3jkKhnsjU/Kkkc0lacr6VdRpCu2CUWa0OKw==} + '@opentelemetry/instrumentation-dataloader@0.26.0': + resolution: {integrity: sha512-P2BgnFfTOarZ5OKPmYfbXfDFjQ4P9WkQ1Jji7yH5/WwB6Wm/knynAoA1rxbjWcDlYupFkyT0M1j6XLzDzy0aCA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-express@0.53.0': - resolution: {integrity: sha512-r/PBafQmFYRjuxLYEHJ3ze1iBnP2GDA1nXOSS6E02KnYNZAVjj6WcDA1MSthtdAUUK0XnotHvvWM8/qz7DMO5A==} + '@opentelemetry/instrumentation-express@0.57.0': + resolution: {integrity: sha512-HAdx/o58+8tSR5iW+ru4PHnEejyKrAy9fYFhlEI81o10nYxrGahnMAHWiSjhDC7UQSY3I4gjcPgSKQz4rm/asg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fs@0.24.0': - resolution: {integrity: sha512-HjIxJ6CBRD770KNVaTdMXIv29Sjz4C1kPCCK5x1Ujpc6SNnLGPqUVyJYZ3LUhhnHAqdbrl83ogVWjCgeT4Q0yw==} + '@opentelemetry/instrumentation-fs@0.28.0': + resolution: {integrity: sha512-FFvg8fq53RRXVBRHZViP+EMxMR03tqzEGpuq55lHNbVPyFklSVfQBN50syPhK5UYYwaStx0eyCtHtbRreusc5g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-generic-pool@0.48.0': - resolution: {integrity: sha512-TLv/On8pufynNR+pUbpkyvuESVASZZKMlqCm4bBImTpXKTpqXaJJ3o/MUDeMlM91rpen+PEv2SeyOKcHCSlgag==} + '@opentelemetry/instrumentation-generic-pool@0.52.0': + resolution: {integrity: sha512-ISkNcv5CM2IwvsMVL31Tl61/p2Zm2I2NAsYq5SSBgOsOndT0TjnptjufYVScCnD5ZLD1tpl4T3GEYULLYOdIdQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.52.0': - resolution: {integrity: sha512-3fEJ8jOOMwopvldY16KuzHbRhPk8wSsOTSF0v2psmOCGewh6ad+ZbkTx/xyUK9rUdUMWAxRVU0tFpj4Wx1vkPA==} + '@opentelemetry/instrumentation-graphql@0.56.0': + resolution: {integrity: sha512-IPvNk8AFoVzTAM0Z399t34VDmGDgwT6rIqCUug8P9oAGerl2/PEIYMPOl/rerPGu+q8gSWdmbFSjgg7PDVRd3Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-hapi@0.51.0': - resolution: {integrity: sha512-qyf27DaFNL1Qhbo/da+04MSCw982B02FhuOS5/UF+PMhM61CcOiu7fPuXj8TvbqyReQuJFljXE6UirlvoT/62g==} + '@opentelemetry/instrumentation-hapi@0.55.0': + resolution: {integrity: sha512-prqAkRf9e4eEpy4G3UcR32prKE8NLNlA90TdEU1UsghOTg0jUvs40Jz8LQWFEs5NbLbXHYGzB4CYVkCI8eWEVQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.204.0': - resolution: {integrity: sha512-1afJYyGRA4OmHTv0FfNTrTAzoEjPQUYgd+8ih/lX0LlZBnGio/O80vxA0lN3knsJPS7FiDrsDrWq25K7oAzbkw==} + '@opentelemetry/instrumentation-http@0.208.0': + resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.52.0': - resolution: {integrity: sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg==} + '@opentelemetry/instrumentation-ioredis@0.56.0': + resolution: {integrity: sha512-XSWeqsd3rKSsT3WBz/JKJDcZD4QYElZEa0xVdX8f9dh4h4QgXhKRLorVsVkK3uXFbC2sZKAS2Ds+YolGwD83Dg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-kafkajs@0.14.0': - resolution: {integrity: sha512-kbB5yXS47dTIdO/lfbbXlzhvHFturbux4EpP0+6H78Lk0Bn4QXiZQW7rmZY1xBCY16mNcCb8Yt0mhz85hTnSVA==} + '@opentelemetry/instrumentation-kafkajs@0.18.0': + resolution: {integrity: sha512-KCL/1HnZN5zkUMgPyOxfGjLjbXjpd4odDToy+7c+UsthIzVLFf99LnfIBE8YSSrYE4+uS7OwJMhvhg3tWjqMBg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-knex@0.49.0': - resolution: {integrity: sha512-NKsRRT27fbIYL4Ix+BjjP8h4YveyKc+2gD6DMZbr5R5rUeDqfC8+DTfIt3c3ex3BIc5Vvek4rqHnN7q34ZetLQ==} + '@opentelemetry/instrumentation-knex@0.53.0': + resolution: {integrity: sha512-xngn5cH2mVXFmiT1XfQ1aHqq1m4xb5wvU6j9lSgLlihJ1bXzsO543cpDwjrZm2nMrlpddBf55w8+bfS4qDh60g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-koa@0.52.0': - resolution: {integrity: sha512-JJSBYLDx/mNSy8Ibi/uQixu2rH0bZODJa8/cz04hEhRaiZQoeJ5UrOhO/mS87IdgVsHrnBOsZ6vDu09znupyuA==} + '@opentelemetry/instrumentation-koa@0.57.0': + resolution: {integrity: sha512-3JS8PU/D5E3q295mwloU2v7c7/m+DyCqdu62BIzWt+3u9utjxC9QS7v6WmUNuoDN3RM+Q+D1Gpj13ERo+m7CGg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@opentelemetry/api': ^1.9.0 - '@opentelemetry/instrumentation-lru-memoizer@0.49.0': - resolution: {integrity: sha512-ctXu+O/1HSadAxtjoEg2w307Z5iPyLOMM8IRNwjaKrIpNAthYGSOanChbk1kqY6zU5CrpkPHGdAT6jk8dXiMqw==} + '@opentelemetry/instrumentation-lru-memoizer@0.53.0': + resolution: {integrity: sha512-LDwWz5cPkWWr0HBIuZUjslyvijljTwmwiItpMTHujaULZCxcYE9eU44Qf/pbVC8TulT0IhZi+RoGvHKXvNhysw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongodb@0.57.0': - resolution: {integrity: sha512-KD6Rg0KSHWDkik+qjIOWoksi1xqSpix8TSPfquIK1DTmd9OTFb5PHmMkzJe16TAPVEuElUW8gvgP59cacFcrMQ==} + '@opentelemetry/instrumentation-mongodb@0.61.0': + resolution: {integrity: sha512-OV3i2DSoY5M/pmLk+68xr5RvkHU8DRB3DKMzYJdwDdcxeLs62tLbkmRyqJZsYf3Ht7j11rq35pHOWLuLzXL7pQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.51.0': - resolution: {integrity: sha512-gwWaAlhhV2By7XcbyU3DOLMvzsgeaymwP/jktDC+/uPkCmgB61zurwqOQdeiRq9KAf22Y2dtE5ZLXxytJRbEVA==} + '@opentelemetry/instrumentation-mongoose@0.55.0': + resolution: {integrity: sha512-5afj0HfF6aM6Nlqgu6/PPHFk8QBfIe3+zF9FGpX76jWPS0/dujoEYn82/XcLSaW5LPUDW8sni+YeK0vTBNri+w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql2@0.51.0': - resolution: {integrity: sha512-zT2Wg22Xn43RyfU3NOUmnFtb5zlDI0fKcijCj9AcK9zuLZ4ModgtLXOyBJSSfO+hsOCZSC1v/Fxwj+nZJFdzLQ==} + '@opentelemetry/instrumentation-mysql2@0.55.0': + resolution: {integrity: sha512-0cs8whQG55aIi20gnK8B7cco6OK6N+enNhW0p5284MvqJ5EPi+I1YlWsWXgzv/V2HFirEejkvKiI4Iw21OqDWg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql@0.50.0': - resolution: {integrity: sha512-duKAvMRI3vq6u9JwzIipY9zHfikN20bX05sL7GjDeLKr2qV0LQ4ADtKST7KStdGcQ+MTN5wghWbbVdLgNcB3rA==} + '@opentelemetry/instrumentation-mysql@0.54.0': + resolution: {integrity: sha512-bqC1YhnwAeWmRzy1/Xf9cDqxNG2d/JDkaxnqF5N6iJKN1eVWI+vg7NfDkf52/Nggp3tl1jcC++ptC61BD6738A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.57.0': - resolution: {integrity: sha512-dWLGE+r5lBgm2A8SaaSYDE3OKJ/kwwy5WLyGyzor8PLhUL9VnJRiY6qhp4njwhnljiLtzeffRtG2Mf/YyWLeTw==} + '@opentelemetry/instrumentation-pg@0.61.0': + resolution: {integrity: sha512-UeV7KeTnRSM7ECHa3YscoklhUtTQPs6V6qYpG283AB7xpnPGCUCUfECFT9jFg6/iZOQTt3FHkB1wGTJCNZEvPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-redis@0.53.0': - resolution: {integrity: sha512-WUHV8fr+8yo5RmzyU7D5BIE1zwiaNQcTyZPwtxlfr7px6NYYx7IIpSihJK7WA60npWynfxxK1T67RAVF0Gdfjg==} + '@opentelemetry/instrumentation-redis@0.57.0': + resolution: {integrity: sha512-bCxTHQFXzrU3eU1LZnOZQ3s5LURxQPDlU3/upBzlWY77qOI1GZuGofazj3jtzjctMJeBEJhNwIFEgRPBX1kp/Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-tedious@0.23.0': - resolution: {integrity: sha512-3TMTk/9VtlRonVTaU4tCzbg4YqW+Iq/l5VnN2e5whP6JgEg/PKfrGbqQ+CxQWNLfLaQYIUgEZqAn5gk/inh1uQ==} + '@opentelemetry/instrumentation-tedious@0.27.0': + resolution: {integrity: sha512-jRtyUJNZppPBjPae4ZjIQ2eqJbcRaRfJkr0lQLHFmOU/no5A6e9s1OHLd5XZyZoBJ/ymngZitanyRRA5cniseA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-undici@0.15.0': - resolution: {integrity: sha512-sNFGA/iCDlVkNjzTzPRcudmI11vT/WAfAguRdZY9IspCw02N4WSC72zTuQhSMheh2a1gdeM9my1imnKRvEEvEg==} + '@opentelemetry/instrumentation-undici@0.19.0': + resolution: {integrity: sha512-Pst/RhR61A2OoZQZkn6OLpdVpXp6qn3Y92wXa6umfJe9rV640r4bc6SWvw4pPN6DiQqPu2c8gnSSZPDtC6JlpQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.7.0 - '@opentelemetry/instrumentation@0.204.0': - resolution: {integrity: sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==} + '@opentelemetry/instrumentation@0.208.0': + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.57.2': - resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/redis-common@0.38.0': - resolution: {integrity: sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==} + '@opentelemetry/redis-common@0.38.2': + resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.1.0': - resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.1.0': - resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -2428,8 +2418,8 @@ packages: resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} engines: {node: '>=14'} - '@opentelemetry/sql-common@0.41.0': - resolution: {integrity: sha512-pmzXctVbEERbqSfiAgdes9Y63xjoOyXcD7B6IXBkVb+vbM7M9U98mn33nGXxPf4dfYR0M+vhcKRZmbSJ7HfqFA==} + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -2526,98 +2516,98 @@ packages: '@oxc-project/types@0.74.0': resolution: {integrity: sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ==} - '@oxc-resolver/binding-android-arm-eabi@11.8.2': - resolution: {integrity: sha512-7hykBf8S24IRbO4ueulT9SfYQjTeSOOimKc/CQrWXIWQy1WTePXSNcPq2RkVHO7DdLM8p8X4DVPYy+850Bo93g==} + '@oxc-resolver/binding-android-arm-eabi@11.14.0': + resolution: {integrity: sha512-jB47iZ/thvhE+USCLv+XY3IknBbkKr/p7OBsQDTHode/GPw+OHRlit3NQ1bjt1Mj8V2CS7iHdSDYobZ1/0gagQ==} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.8.2': - resolution: {integrity: sha512-y41bxENMjlFuLSLCPWd4A+1PR7T5rU9+e7+4alje3sHgrpRmS3hIU+b1Cvck4qmcUgd0I98NmYxRM65kXGEObQ==} + '@oxc-resolver/binding-android-arm64@11.14.0': + resolution: {integrity: sha512-XFJ9t7d/Cz+dWLyqtTy3Xrekz+qqN4hmOU2iOUgr7u71OQsPUHIIeS9/wKanEK0l413gPwapIkyc5x9ltlOtyw==} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.8.2': - resolution: {integrity: sha512-P/Zobk9OwQAblAMeiVyOtuX2LjGN8oq5HonvN3mp9S6Kx1GKxREbf5qW+g24Rvhf5WS7et+EmopUGRHSdAItGQ==} + '@oxc-resolver/binding-darwin-arm64@11.14.0': + resolution: {integrity: sha512-gwehBS9smA1mzK8frDsmUCHz+6baJVwkKF6qViHhoqA3kRKvIZ3k6WNP4JmF19JhOiGxRcoPa8gZRfzNgXwP2A==} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.8.2': - resolution: {integrity: sha512-EMAQoO9uTiz2H0z71bVzTL77eoBAlN5+KD7HUc9ayYJ5TprU+Oeaml4y4fmsFyspSPN/vGJzEvOWl5GR0adwtw==} + '@oxc-resolver/binding-darwin-x64@11.14.0': + resolution: {integrity: sha512-5wwJvfuoahKiAqqAsMLOI28rqdh3P2K7HkjIWUXNMWAZq6ErX0L5rwJzu6T32+Zxw3k18C7R9IS4wDq/3Ar+6w==} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.8.2': - resolution: {integrity: sha512-Fzeupf4tH9woMm6O/pirEtuzO5docwTrs747Nxqh33OSkz7GbrevyDpx1Q1pc2l3JA2BlDX4zm18tW5ys65bjA==} + '@oxc-resolver/binding-freebsd-x64@11.14.0': + resolution: {integrity: sha512-MWTt+LOQNcQ6fa+Uu5VikkihLi1PSIrQqqp0QD44k2AORasNWl0jRGBTcMSBIgNe82qEQWYvlGzvOEEOBp01Og==} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.8.2': - resolution: {integrity: sha512-r9IiPTwc5STC2JahU/rfkbO2BE14MqAVmFbtF7uW7KFaZX/lUnFltkQ5jpwAgKqcef5aIZTJI95qJ03XZw08Rg==} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.14.0': + resolution: {integrity: sha512-b6/IBqYrS3o0XiLVBsnex/wK8pTTK+hbGfAMOHVU6p7DBpwPPLgC/tav4IXoOIUCssTFz7aWh/xtUok0swn8VQ==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.8.2': - resolution: {integrity: sha512-Q5D8FbxOyQYcWn5s9yv+DyFvcMSUXE87hmL9WG6ICdNZiMUA8DmIbzK1xEnOtDjorEFU44bwH3I9SnqL1kyOsg==} + '@oxc-resolver/binding-linux-arm-musleabihf@11.14.0': + resolution: {integrity: sha512-o2Qh5+y5YoqVK6YfzkalHdpmQ5bkbGGxuLg1pZLQ1Ift0x+Vix7DaFEpdCl5Z9xvYXogd/TwOlL0TPl4+MTFLA==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.8.2': - resolution: {integrity: sha512-8g2Y72gavZ8fesZD22cKo0Z8g8epynwShu7M+wpAoOq432IGUyUxPUKB2/nvyogPToaAlb1OsRiX/za8W4h8Aw==} + '@oxc-resolver/binding-linux-arm64-gnu@11.14.0': + resolution: {integrity: sha512-lk8mCSg0Tg4sEG73RiPjb7keGcEPwqQnBHX3Z+BR2SWe+qNHpoHcyFMNafzSvEC18vlxC04AUSoa6kJl/C5zig==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-arm64-musl@11.8.2': - resolution: {integrity: sha512-N3BPWnIDRmHn/xPDZGKnzFwWxwH1hvs3aVnw4jvMAYarPNDZfbAY+fjHSIwkypV+ozMoJ5lK5PzRO5BOtEx2oQ==} + '@oxc-resolver/binding-linux-arm64-musl@11.14.0': + resolution: {integrity: sha512-KykeIVhCM7pn93ABa0fNe8vk4XvnbfZMELne2s6P9tdJH9KMBsCFBi7a2BmSdUtTqWCAJokAcm46lpczU52Xaw==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-ppc64-gnu@11.8.2': - resolution: {integrity: sha512-AXW2AyjENmzNuZD3Z2TO1QWoZzfULWR1otDzw/+MAVMRXBy3W50XxDqNAflRiLB4o0aI0oDTwMfeyuhVv9Ur8Q==} + '@oxc-resolver/binding-linux-ppc64-gnu@11.14.0': + resolution: {integrity: sha512-QqPPWAcZU/jHAuam4f3zV8OdEkYRPD2XR0peVet3hoMMgsihR3Lhe7J/bLclmod297FG0+OgBYQVMh2nTN6oWA==} cpu: [ppc64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-gnu@11.8.2': - resolution: {integrity: sha512-oX+qxJdqOfrJUkGWmcNpu7wiFs6E7KH6hqUORkMAgl4yW+LZxPTz5P4DHvTqTFMywbs9hXVu2KQrdD8ROrdhMQ==} + '@oxc-resolver/binding-linux-riscv64-gnu@11.14.0': + resolution: {integrity: sha512-DunWA+wafeG3hj1NADUD3c+DRvmyVNqF5LSHVUWA2bzswqmuEZXl3VYBSzxfD0j+UnRTFYLxf27AMptoMsepYg==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-musl@11.8.2': - resolution: {integrity: sha512-TG7LpxXjqlpD1aWnAXw6vMgY74KNV92exPixzEj4AKm4LdGsfnSWYTTJcTQ7deFMYxvBGrZ+qEy8DjGx+5w9GQ==} + '@oxc-resolver/binding-linux-riscv64-musl@11.14.0': + resolution: {integrity: sha512-4SRvwKTTk2k67EQr9Ny4NGf/BhlwggCI1CXwBbA9IV4oP38DH8b+NAPxDY0ySGRsWbPkG92FYOqM4AWzG4GSgA==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-s390x-gnu@11.8.2': - resolution: {integrity: sha512-1PpXMq0KMD3CQPn3v/UqU4NM2JFjry+mLIH1d3iNVL2vlwRt9lxRfpXTiyiFJrtroUIyeKhw0QbHbF2UfnZVKQ==} + '@oxc-resolver/binding-linux-s390x-gnu@11.14.0': + resolution: {integrity: sha512-hZKvkbsurj4JOom//R1Ab2MlC4cGeVm5zzMt4IsS3XySQeYjyMJ5TDZ3J5rQ8bVj3xi4FpJU2yFZ72GApsHQ6A==} cpu: [s390x] os: [linux] - '@oxc-resolver/binding-linux-x64-gnu@11.8.2': - resolution: {integrity: sha512-V1iYhEDbjQzj+o7JgTYVllRgNZ56Tjw0rPBWw03KJQ8Nphy00Vf7AySf22vV0K/93V1lPCgOSbI5/iunRnIfAw==} + '@oxc-resolver/binding-linux-x64-gnu@11.14.0': + resolution: {integrity: sha512-hABxQXFXJurivw+0amFdeEcK67cF1BGBIN1+sSHzq3TRv4RoG8n5q2JE04Le2n2Kpt6xg4Y5+lcv+rb2mCJLgQ==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-linux-x64-musl@11.8.2': - resolution: {integrity: sha512-2hYNXEZSUM7qLEk4uuY3GmMqLU+860v+8PzbloVvRRjTWtHsLZyB5w+5p2gel38eaTcSYfZ2zvp3xcSpKDAbaw==} + '@oxc-resolver/binding-linux-x64-musl@11.14.0': + resolution: {integrity: sha512-Ln73wUB5migZRvC7obAAdqVwvFvk7AUs2JLt4g9QHr8FnqivlsjpUC9Nf2ssrybdjyQzEMjttUxPZz6aKPSAHw==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-wasm32-wasi@11.8.2': - resolution: {integrity: sha512-TjFqB+1siSqhd+S64Hf2qbxqWqtFIlld4DDEVotxOjj5//rX/6uwAL1HWnUHSNIni+wpcyQoXPhO3fBgppCvuA==} + '@oxc-resolver/binding-wasm32-wasi@11.14.0': + resolution: {integrity: sha512-z+NbELmCOKNtWOqEB5qDfHXOSWB3kGQIIehq6nHtZwHLzdVO2oBq6De/ayhY3ygriC1XhgaIzzniY7jgrNl4Kw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.8.2': - resolution: {integrity: sha512-fs0X6RcAC/khWbXIhPaYQjFHkrFVUtC2IOw1QEx2unRoe6M11tlYbY9NHr3VFBC3nwVpodX+b14A7jGMkAQK8A==} + '@oxc-resolver/binding-win32-arm64-msvc@11.14.0': + resolution: {integrity: sha512-Ft0+qd7HSO61qCTLJ4LCdBGZkpKyDj1rG0OVSZL1DxWQoh97m7vEHd7zAvUtw8EcWjOMBQuX4mfRap/x2MOCpQ==} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-ia32-msvc@11.8.2': - resolution: {integrity: sha512-7oEl1ThswVePprRQFc3tzW9IZgVi5xaus/KP3k56eKi2tYpAM0hBvehD8WBsmpgBEb7pe2pI08h9OZveAddt3Q==} + '@oxc-resolver/binding-win32-ia32-msvc@11.14.0': + resolution: {integrity: sha512-o54jYNSfGdPxHSvXEhZg8FOV3K99mJ1f7hb1alRFb+Yec1GQXNrJXxZPIxNMYeFT13kwAWB7zuQ0HZLnDHFxfw==} cpu: [ia32] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.8.2': - resolution: {integrity: sha512-MngRjE/gpQpg3QcnWRqxX5Nbr/vZJSG7oxhXeHUeOhdFgg+0xCuGpDtwqFmGGVKnd6FQg0gKVo1MqDAERLkEPA==} + '@oxc-resolver/binding-win32-x64-msvc@11.14.0': + resolution: {integrity: sha512-j97icaORyM6A7GjgmUzfn7V+KGzVvctRA+eAlJb0c2OQNaETFxl6BXZdnGBDb+6oA0Y4Sr/wnekd1kQ0aVyKGg==} cpu: [x64] os: [win32] @@ -2643,8 +2633,8 @@ packages: resolution: {integrity: sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ==} engines: {node: '>=14'} - '@prisma/instrumentation@6.15.0': - resolution: {integrity: sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A==} + '@prisma/instrumentation@6.19.0': + resolution: {integrity: sha512-QcuYy25pkXM8BJ37wVFBO7Zh34nyRV1GOb2n3lPkkbRYfl4hWl3PTcImP41P0KrzVXfa/45p6eVCos27x3exIg==} peerDependencies: '@opentelemetry/api': ^1.8 @@ -3102,12 +3092,12 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sentry-internal/browser-utils@10.23.0': - resolution: {integrity: sha512-FUak8FH51TnGrx2i31tgqun0VsbDCVQS7dxWnUZHdi+0hpnFoq9+wBHY+qrOQjaInZSz3crIifYv3z7SEzD0Jg==} + '@sentry-internal/browser-utils@10.27.0': + resolution: {integrity: sha512-17tO6AXP+rmVQtLJ3ROQJF2UlFmvMWp7/8RDT5x9VM0w0tY31z8Twc0gw2KA7tcDxa5AaHDUbf9heOf+R6G6ow==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.23.0': - resolution: {integrity: sha512-+HWC9VTPICsFX/lIPoBU9GxTaJZVXJcukP+qGxj+j/8q/Dy1w22JHDWcJbZiaW4kWWlz7VbA0KVKS3grD+e9aA==} + '@sentry-internal/feedback@10.27.0': + resolution: {integrity: sha512-UecsIDJcv7VBwycge/MDvgSRxzevDdcItE1i0KSwlPz00rVVxLY9kV28PJ4I2E7r6/cIaP9BkbWegCEcv09NuA==} engines: {node: '>=18'} '@sentry-internal/global-search@1.0.0': @@ -3120,12 +3110,12 @@ packages: resolution: {integrity: sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.23.0': - resolution: {integrity: sha512-GLNY8JPcMI6xhQ5FHiYO/W/3flrwZMt4CI/E3jDRNujYWbCrca60MRke6k7Zm1qi9rZ1FuhVWZ6BAFc4vwXnSg==} + '@sentry-internal/replay-canvas@10.27.0': + resolution: {integrity: sha512-inhsRYSVBpu3BI1kZphXj6uB59baJpYdyHeIPCiTfdFNBE5tngNH0HS/aedZ1g9zICw290lwvpuyrWJqp4VBng==} engines: {node: '>=18'} - '@sentry-internal/replay@10.23.0': - resolution: {integrity: sha512-5yPD7jVO2JY8+JEHXep0Bf/ugp4rmxv5BkHIcSAHQsKSPhziFks2x+KP+6M8hhbF1WydqAaDYlGjrkL2yspHqA==} + '@sentry-internal/replay@10.27.0': + resolution: {integrity: sha512-tKSzHq1hNzB619Ssrqo25cqdQJ84R3xSSLsUWEnkGO/wcXJvpZy94gwdoS+KmH18BB1iRRRGtnMxZcUkiPSesw==} engines: {node: '>=18'} '@sentry-internal/rrdom@2.40.0': @@ -3146,72 +3136,72 @@ packages: '@sentry-internal/rrweb@2.40.0': resolution: {integrity: sha512-niFva5QmCTfavotLvIeFSvO0rfzbJwW04igcPaWAqTDATi+Xife27iBeVMBmjpHEWygGYkBaGyBQUUi8zUdAyg==} - '@sentry/babel-plugin-component-annotate@4.4.0': - resolution: {integrity: sha512-Pzjpn9MZg6yR61ThJgOoD28dLNCj457O0/t8d276K+Bzf8iOZKbrNO4sltp1vUB1yqhV+ulvIZO8xu8ABohtsg==} + '@sentry/babel-plugin-component-annotate@4.6.1': + resolution: {integrity: sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==} engines: {node: '>= 14'} - '@sentry/browser@10.23.0': - resolution: {integrity: sha512-9hViLfYONxRJykOhJQ3ZHQ758t1wQIsxEC7mTsydbDm+m12LgbBtXbfgcypWHlom5Yvb+wg6W+31bpdGnATglw==} + '@sentry/browser@10.27.0': + resolution: {integrity: sha512-G8q362DdKp9y1b5qkQEmhTFzyWTOVB0ps1rflok0N6bVA75IEmSDX1pqJsNuY3qy14VsVHYVwQBJQsNltQLS0g==} engines: {node: '>=18'} - '@sentry/bundler-plugin-core@4.4.0': - resolution: {integrity: sha512-WTGhgwxzyolzOg0sudULK0rRgLndtsEiBt4QwltKW/WYArMtFyf286aZx19uQ+rD+bSx3Il81SD23nqDOTtnzg==} + '@sentry/bundler-plugin-core@4.6.1': + resolution: {integrity: sha512-WPeRbnMXm927m4Kr69NTArPfI+p5/34FHftdCRI3LFPMyhZDzz6J3wLy4hzaVUgmMf10eLzmq2HGEMvpQmdynA==} engines: {node: '>= 14'} - '@sentry/cli-darwin@2.56.1': - resolution: {integrity: sha512-zfhT8MrvB5x/xRdIVGwg+sG0Cx3i0G6RH2zCrdQ/moWn8TfkwsM0O1k/AxpwbpcRfAHCkVb04CU/yKciKwg2KA==} + '@sentry/cli-darwin@2.58.2': + resolution: {integrity: sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==} engines: {node: '>=10'} os: [darwin] - '@sentry/cli-linux-arm64@2.56.1': - resolution: {integrity: sha512-AypXIwZvOMJb9RgjI/98hTAd06FcOjqjIm6G9IR0OI4pJCOcaAXz9NKXdJqxpZd7phSMJnD+Bx/8iYOUPeY73A==} + '@sentry/cli-linux-arm64@2.58.2': + resolution: {integrity: sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==} engines: {node: '>=10'} cpu: [arm64] os: [linux, freebsd, android] - '@sentry/cli-linux-arm@2.56.1': - resolution: {integrity: sha512-fNB/Ng11HrkGOSEIDg+fc3zfTCV7q6kJddp6ndK3QlYFsCffRSnclaX1SMp+mqxdWkHqe1kkp85OY8G/x5uAWw==} + '@sentry/cli-linux-arm@2.58.2': + resolution: {integrity: sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==} engines: {node: '>=10'} cpu: [arm] os: [linux, freebsd, android] - '@sentry/cli-linux-i686@2.56.1': - resolution: {integrity: sha512-vnH+WJEsUq7Lf7xc9udzE/M4hoDXXsniFFYr/7BvdnXtCQlNNaWFMXHbEDYAql3baIlHkWoG8cEHWuB/YKyniw==} + '@sentry/cli-linux-i686@2.58.2': + resolution: {integrity: sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==} engines: {node: '>=10'} cpu: [x86, ia32] os: [linux, freebsd, android] - '@sentry/cli-linux-x64@2.56.1': - resolution: {integrity: sha512-3/BlKe5Vdnia36MeovghHJD8lbcum5TFIxLp+PSfH2sVb09+5Jo0L95oRTI2JkD8Fs+QNssvTqTxJj5eIo/n+A==} + '@sentry/cli-linux-x64@2.58.2': + resolution: {integrity: sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==} engines: {node: '>=10'} cpu: [x64] os: [linux, freebsd, android] - '@sentry/cli-win32-arm64@2.56.1': - resolution: {integrity: sha512-Gg8RV7CV7Tz4fiR1EN1Af5AVhJsnEXiZvfvfQXI4lp51MKAhcxZIMtEfg9HaWsn3Dm/wgwYBinyeywfWbTXYDg==} + '@sentry/cli-win32-arm64@2.58.2': + resolution: {integrity: sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@sentry/cli-win32-i686@2.56.1': - resolution: {integrity: sha512-6u6a060yC3i76Ze1apqgWr5luQSyhuD5ND84eWfh/UbddsEa42UHjoVHOiBwmpZqf/hvNZAtzLnE4NCvU4zOMg==} + '@sentry/cli-win32-i686@2.58.2': + resolution: {integrity: sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==} engines: {node: '>=10'} cpu: [x86, ia32] os: [win32] - '@sentry/cli-win32-x64@2.56.1': - resolution: {integrity: sha512-11cdflajBrDWlRZqI9MOu7ok2vnPzFjKmbU3YvBYWQapNE+HHAsWdsRL/u/P1RmU62vj7Y42iSUcj6x1SNrdPw==} + '@sentry/cli-win32-x64@2.58.2': + resolution: {integrity: sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@sentry/cli@2.56.1': - resolution: {integrity: sha512-VDAIg+gmjNtJS5VUZQMDSK9RaKC9hYQi3PoXpNa+owNfQNk60bCi8z8jkbWRcKbNGn3V51WqvrQAqLoNAdPc9w==} + '@sentry/cli@2.58.2': + resolution: {integrity: sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==} engines: {node: '>= 10'} hasBin: true - '@sentry/core@10.23.0': - resolution: {integrity: sha512-4aZwu6VnSHWDplY5eFORcVymhfvS/P6BRfK81TPnG/ReELaeoykKjDwR+wC4lO7S0307Vib9JGpszjsEZw245g==} + '@sentry/core@10.27.0': + resolution: {integrity: sha512-Zc68kdH7tWTDtDbV1zWIbo3Jv0fHAU2NsF5aD2qamypKgfSIMSbWVxd22qZyDBkaX8gWIPm/0Sgx6aRXRBXrYQ==} engines: {node: '>=18'} '@sentry/jest-environment@6.1.0': @@ -3221,39 +3211,39 @@ packages: '@sentry/profiling-node': '>=8' jest: '>=29' - '@sentry/node-core@10.23.0': - resolution: {integrity: sha512-3vhttO19pta7zIuecSrLoPTVN7NdjKtb/WK241H8znwKxukx3fj3M6+upN+JQtC6pERO3HfQwBpMMT9RMCUr3Q==} + '@sentry/node-core@10.27.0': + resolution: {integrity: sha512-Dzo1I64Psb7AkpyKVUlR9KYbl4wcN84W4Wet3xjLmVKMgrCo2uAT70V4xIacmoMH5QLZAx0nGfRy9yRCd4nzBg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 || ^2.2.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 || ^2.2.0 '@opentelemetry/instrumentation': '>=0.57.1 <1' - '@opentelemetry/resources': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/resources': ^1.30.1 || ^2.1.0 || ^2.2.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 || ^2.2.0 '@opentelemetry/semantic-conventions': ^1.37.0 - '@sentry/node@10.23.0': - resolution: {integrity: sha512-5PwJJ1zZ89tB8hrjTVKNE4fIGtSXlR+Mdg2u1Nm2FJ2Vj1Ac6JArLiRzMqoq/pA7vwgZMoHwviDAA+PfpJ0Agg==} + '@sentry/node@10.27.0': + resolution: {integrity: sha512-1cQZ4+QqV9juW64Jku1SMSz+PoZV+J59lotz4oYFvCNYzex8hRAnDKvNiKW1IVg5mEEkz98mg1fvcUtiw7GTiQ==} engines: {node: '>=18'} - '@sentry/opentelemetry@10.23.0': - resolution: {integrity: sha512-ZbSB5y8K8YXp5+sBp2w7xHsNLv9EglJRTRqWMi2ncovXy4jcvo+pSreiZu68nSGvxX25brYKDw19vl+tnmqZVg==} + '@sentry/opentelemetry@10.27.0': + resolution: {integrity: sha512-z2vXoicuGiqlRlgL9HaYJgkin89ncMpNQy0Kje6RWyhpzLe8BRgUXlgjux7WrSrcbopDdC1OttSpZsJ/Wjk7fg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 || ^2.2.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 || ^2.2.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 || ^2.2.0 '@opentelemetry/semantic-conventions': ^1.37.0 - '@sentry/profiling-node@10.23.0': - resolution: {integrity: sha512-Uvy/sZwdYHBkwg53mXV7BC0fyqNA50w2RZuoDOPwNao/gO+/nDB63gT2HoMt1eNrJWGRnmrSl1/GPDoR/lZq0g==} + '@sentry/profiling-node@10.27.0': + resolution: {integrity: sha512-IMUdgNaiT7aji6/VDF5F1noY8LPpF3yFD6BjomQz72h0KeUrN/88S5MZNjcY7ZpW7wvI2yahUDLkMk11ScSMXQ==} engines: {node: '>=18'} hasBin: true - '@sentry/react@10.23.0': - resolution: {integrity: sha512-WtDrhs9zF5YAf1DwsIhmS2E1EXx4cA3WeFCzty+rpS7e6XQXk+riAdHvAUZxccHkzv5sxSOCYANFy3J7oUiYcg==} + '@sentry/react@10.27.0': + resolution: {integrity: sha512-xoIRBlO1IhLX/O9aQgVYW1F3Qhw8TdkOiZjh6mrPsnCpBLufsQ4aS1nDQi9miZuWeslW0s2zNy0ACBpICZR/sw==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x @@ -3270,8 +3260,8 @@ packages: peerDependencies: react: '>=18' - '@sentry/webpack-plugin@4.4.0': - resolution: {integrity: sha512-s9Js4v++pbZaKu6ddG1LSXbSKfM71UxkS6PzmOWj4HyTHdiZr+469tbdanTJwz8XO87neFAP1mteuo1Cur3iHg==} + '@sentry/webpack-plugin@4.6.1': + resolution: {integrity: sha512-CJgT/t2pQWsPsMx9VJ86goU/orCQhL2HhDj5ZYBol6fPPoEGeTqKOPCnv/xsbCAfGSp1uHpyRLTA/Gx96u7VVA==} engines: {node: '>= 14'} peerDependencies: webpack: '>=4.40.0' @@ -3623,8 +3613,8 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - '@types/pg@8.15.5': - resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} '@types/prismjs@1.26.0': resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} @@ -3691,9 +3681,6 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/shimmer@1.2.0': - resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} - '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} @@ -5602,14 +5589,14 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} - global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -5870,8 +5857,8 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-in-the-middle@1.14.2: - resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + import-in-the-middle@2.0.0: + resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} @@ -6337,8 +6324,8 @@ packages: node-notifier: optional: true - jiti@2.5.1: - resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true js-base64@3.7.7: @@ -6364,6 +6351,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -6445,13 +6436,13 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - knip@5.64.0: - resolution: {integrity: sha512-UqDlVXXacGy5YL+PXKrolqRpC7DkGTYs+to67KmWBHIUrTh8SX9gQoGNdFsNZtbj4pCdM/RmC/Rbze555+MhSA==} + knip@5.71.0: + resolution: {integrity: sha512-hwgdqEJ+7DNJ5jE8BCPu7b57TY7vUwP6MzWYgCgPpg6iPCee/jKPShDNIlFER2koti4oz5xF88VJbKCb4Wl71g==} engines: {node: '>=18.18.0'} hasBin: true peerDependencies: '@types/node': '>=18' - typescript: '>=5.0.4' + typescript: '>=5.0.4 <7' known-css-properties@0.34.0: resolution: {integrity: sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==} @@ -6821,10 +6812,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} @@ -6836,10 +6823,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} - engines: {node: '>=8'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -6917,11 +6900,6 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true - napi-postinstall@0.3.3: - resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -7110,8 +7088,8 @@ packages: resolution: {integrity: sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw==} engines: {node: '>=20.0.0'} - oxc-resolver@11.8.2: - resolution: {integrity: sha512-SM31gnF1l4T8YA7dkAcBhA+jc336bc8scy0Tetz6ndzGmV6c0R99SRnx6In0V5ffwvn1Isjo9I9EGSLF4xi3TA==} + oxc-resolver@11.14.0: + resolution: {integrity: sha512-i4wNrqhOd+4YdHJfHglHtFiqqSxXuzFA+RUqmmWN1aMD3r1HqUSrIhw17tSO4jwKfhLs9uw1wzFPmvMsWacStg==} p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} @@ -7706,9 +7684,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - require-in-the-middle@7.5.2: - resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} - engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} @@ -7888,9 +7866,6 @@ packages: shiki@3.7.0: resolution: {integrity: sha512-ZcI4UT9n6N2pDuM2n3Jbk0sR4Swzq43nLPgS/4h0E3B/NrFn2HKElrDtceSf8Zx/OWYOo7G1SAtBLypCp+YXqg==} - shimmer@1.2.1: - resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -7929,8 +7904,8 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smol-toml@1.4.2: - resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} + smol-toml@1.5.2: + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} engines: {node: '>= 18'} socket.io-adapter@2.5.5: @@ -8108,8 +8083,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-json-comments@5.0.2: - resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} style-loader@4.0.0: @@ -8868,6 +8843,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + zrender@6.0.0: resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} @@ -10059,10 +10037,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@emotion/eslint-plugin@11.12.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@emotion/eslint-plugin@11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -10204,14 +10182,14 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.34.0(jiti@2.5.1))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.34.0(jiti@2.6.1))': dependencies: - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.5.1))': + '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.6.1))': dependencies: - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -10755,7 +10733,7 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true - '@napi-rs/wasm-runtime@1.0.5': + '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.5.0 '@emnapi/runtime': 1.5.0 @@ -10888,257 +10866,236 @@ snapshots: '@one-ini/wasm@0.1.1': {} - '@opentelemetry/api-logs@0.204.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.57.2': + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/instrumentation-amqplib@0.51.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-amqplib@0.55.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-connect@0.48.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-connect@0.52.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-dataloader@0.22.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-dataloader@0.26.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-express@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-express@0.57.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fs@0.24.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-fs@0.28.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-generic-pool@0.48.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-generic-pool@0.52.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.52.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-graphql@0.56.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-hapi@0.51.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-hapi@0.55.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-http@0.204.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.52.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.56.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.0 - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-kafkajs@0.14.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-kafkajs@0.18.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-knex@0.49.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-knex@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-koa@0.52.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-koa@0.57.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-lru-memoizer@0.49.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-lru-memoizer@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongodb@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongodb@0.61.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.51.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongoose@0.55.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql2@0.51.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql2@0.55.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql@0.50.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql@0.54.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@types/mysql': 2.15.27 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.61.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.5 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) + '@types/pg': 8.15.6 '@types/pg-pool': 2.0.6 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-redis@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-redis@0.57.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.0 + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-tedious@0.23.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-tedious@0.27.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-undici@0.15.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-undici@0.19.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.204.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.204.0 - import-in-the-middle: 1.14.2 - require-in-the-middle: 7.5.2 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.14.2 - require-in-the-middle: 7.5.2 - semver: 7.7.2 - shimmer: 1.2.1 + '@opentelemetry/api-logs': 0.208.0 + import-in-the-middle: 2.0.0 + require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/redis-common@0.38.0': {} + '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 '@opentelemetry/semantic-conventions@1.37.0': {} - '@opentelemetry/sql-common@0.41.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@oxc-parser/binding-android-arm64@0.74.0': optional: true @@ -11189,63 +11146,63 @@ snapshots: '@oxc-project/types@0.74.0': {} - '@oxc-resolver/binding-android-arm-eabi@11.8.2': + '@oxc-resolver/binding-android-arm-eabi@11.14.0': optional: true - '@oxc-resolver/binding-android-arm64@11.8.2': + '@oxc-resolver/binding-android-arm64@11.14.0': optional: true - '@oxc-resolver/binding-darwin-arm64@11.8.2': + '@oxc-resolver/binding-darwin-arm64@11.14.0': optional: true - '@oxc-resolver/binding-darwin-x64@11.8.2': + '@oxc-resolver/binding-darwin-x64@11.14.0': optional: true - '@oxc-resolver/binding-freebsd-x64@11.8.2': + '@oxc-resolver/binding-freebsd-x64@11.14.0': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.8.2': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.14.0': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.8.2': + '@oxc-resolver/binding-linux-arm-musleabihf@11.14.0': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.8.2': + '@oxc-resolver/binding-linux-arm64-gnu@11.14.0': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.8.2': + '@oxc-resolver/binding-linux-arm64-musl@11.14.0': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.8.2': + '@oxc-resolver/binding-linux-ppc64-gnu@11.14.0': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.8.2': + '@oxc-resolver/binding-linux-riscv64-gnu@11.14.0': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.8.2': + '@oxc-resolver/binding-linux-riscv64-musl@11.14.0': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.8.2': + '@oxc-resolver/binding-linux-s390x-gnu@11.14.0': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.8.2': + '@oxc-resolver/binding-linux-x64-gnu@11.14.0': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.8.2': + '@oxc-resolver/binding-linux-x64-musl@11.14.0': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.8.2': + '@oxc-resolver/binding-wasm32-wasi@11.14.0': dependencies: - '@napi-rs/wasm-runtime': 1.0.5 + '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.8.2': + '@oxc-resolver/binding-win32-arm64-msvc@11.14.0': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.8.2': + '@oxc-resolver/binding-win32-ia32-msvc@11.14.0': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.8.2': + '@oxc-resolver/binding-win32-x64-msvc@11.14.0': optional: true '@peggyjs/from-mem@1.3.4': @@ -11265,10 +11222,10 @@ snapshots: dependencies: oxc-parser: 0.74.0 - '@prisma/instrumentation@6.15.0(@opentelemetry/api@1.9.0)': + '@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -12019,13 +11976,13 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@sentry-internal/browser-utils@10.23.0': + '@sentry-internal/browser-utils@10.27.0': dependencies: - '@sentry/core': 10.23.0 + '@sentry/core': 10.27.0 - '@sentry-internal/feedback@10.23.0': + '@sentry-internal/feedback@10.27.0': dependencies: - '@sentry/core': 10.23.0 + '@sentry/core': 10.27.0 '@sentry-internal/global-search@1.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -12045,15 +12002,15 @@ snapshots: detect-libc: 2.0.4 node-abi: 3.75.0 - '@sentry-internal/replay-canvas@10.23.0': + '@sentry-internal/replay-canvas@10.27.0': dependencies: - '@sentry-internal/replay': 10.23.0 - '@sentry/core': 10.23.0 + '@sentry-internal/replay': 10.27.0 + '@sentry/core': 10.27.0 - '@sentry-internal/replay@10.23.0': + '@sentry-internal/replay@10.27.0': dependencies: - '@sentry-internal/browser-utils': 10.23.0 - '@sentry/core': 10.23.0 + '@sentry-internal/browser-utils': 10.27.0 + '@sentry/core': 10.27.0 '@sentry-internal/rrdom@2.40.0': dependencies: @@ -12088,55 +12045,55 @@ snapshots: fflate: 0.4.8 mitt: 3.0.1 - '@sentry/babel-plugin-component-annotate@4.4.0': {} + '@sentry/babel-plugin-component-annotate@4.6.1': {} - '@sentry/browser@10.23.0': + '@sentry/browser@10.27.0': dependencies: - '@sentry-internal/browser-utils': 10.23.0 - '@sentry-internal/feedback': 10.23.0 - '@sentry-internal/replay': 10.23.0 - '@sentry-internal/replay-canvas': 10.23.0 - '@sentry/core': 10.23.0 + '@sentry-internal/browser-utils': 10.27.0 + '@sentry-internal/feedback': 10.27.0 + '@sentry-internal/replay': 10.27.0 + '@sentry-internal/replay-canvas': 10.27.0 + '@sentry/core': 10.27.0 - '@sentry/bundler-plugin-core@4.4.0(encoding@0.1.13)': + '@sentry/bundler-plugin-core@4.6.1(encoding@0.1.13)': dependencies: '@babel/core': 7.28.0 - '@sentry/babel-plugin-component-annotate': 4.4.0 - '@sentry/cli': 2.56.1(encoding@0.1.13) + '@sentry/babel-plugin-component-annotate': 4.6.1 + '@sentry/cli': 2.58.2(encoding@0.1.13) dotenv: 16.4.5 find-up: 5.0.0 - glob: 9.3.5 + glob: 10.5.0 magic-string: 0.30.8 unplugin: 1.0.1 transitivePeerDependencies: - encoding - supports-color - '@sentry/cli-darwin@2.56.1': + '@sentry/cli-darwin@2.58.2': optional: true - '@sentry/cli-linux-arm64@2.56.1': + '@sentry/cli-linux-arm64@2.58.2': optional: true - '@sentry/cli-linux-arm@2.56.1': + '@sentry/cli-linux-arm@2.58.2': optional: true - '@sentry/cli-linux-i686@2.56.1': + '@sentry/cli-linux-i686@2.58.2': optional: true - '@sentry/cli-linux-x64@2.56.1': + '@sentry/cli-linux-x64@2.58.2': optional: true - '@sentry/cli-win32-arm64@2.56.1': + '@sentry/cli-win32-arm64@2.58.2': optional: true - '@sentry/cli-win32-i686@2.56.1': + '@sentry/cli-win32-i686@2.58.2': optional: true - '@sentry/cli-win32-x64@2.56.1': + '@sentry/cli-win32-x64@2.58.2': optional: true - '@sentry/cli@2.56.1(encoding@0.1.13)': + '@sentry/cli@2.58.2(encoding@0.1.13)': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0(encoding@0.1.13) @@ -12144,103 +12101,103 @@ snapshots: proxy-from-env: 1.1.0 which: 2.0.2 optionalDependencies: - '@sentry/cli-darwin': 2.56.1 - '@sentry/cli-linux-arm': 2.56.1 - '@sentry/cli-linux-arm64': 2.56.1 - '@sentry/cli-linux-i686': 2.56.1 - '@sentry/cli-linux-x64': 2.56.1 - '@sentry/cli-win32-arm64': 2.56.1 - '@sentry/cli-win32-i686': 2.56.1 - '@sentry/cli-win32-x64': 2.56.1 + '@sentry/cli-darwin': 2.58.2 + '@sentry/cli-linux-arm': 2.58.2 + '@sentry/cli-linux-arm64': 2.58.2 + '@sentry/cli-linux-i686': 2.58.2 + '@sentry/cli-linux-x64': 2.58.2 + '@sentry/cli-win32-arm64': 2.58.2 + '@sentry/cli-win32-i686': 2.58.2 + '@sentry/cli-win32-x64': 2.58.2 transitivePeerDependencies: - encoding - supports-color - '@sentry/core@10.23.0': {} + '@sentry/core@10.27.0': {} - '@sentry/jest-environment@6.1.0(@sentry/node@10.23.0)(@sentry/profiling-node@10.23.0)(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))': + '@sentry/jest-environment@6.1.0(@sentry/node@10.27.0)(@sentry/profiling-node@10.27.0)(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))': dependencies: - '@sentry/node': 10.23.0 - '@sentry/profiling-node': 10.23.0 + '@sentry/node': 10.27.0 + '@sentry/profiling-node': 10.27.0 jest: 30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)) - '@sentry/node-core@10.23.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.204.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)': + '@sentry/node-core@10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)': dependencies: '@apm-js-collab/tracing-hooks': 0.3.1 '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@sentry/core': 10.23.0 - '@sentry/opentelemetry': 10.23.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) - import-in-the-middle: 1.14.2 + '@sentry/core': 10.27.0 + '@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) + import-in-the-middle: 2.0.0 transitivePeerDependencies: - supports-color - '@sentry/node@10.23.0': + '@sentry/node@10.27.0': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.24.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.48.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.204.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.14.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.49.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.50.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.51.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.15.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.18.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.61.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.54.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.55.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.61.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.57.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.19.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@prisma/instrumentation': 6.15.0(@opentelemetry/api@1.9.0) - '@sentry/core': 10.23.0 - '@sentry/node-core': 10.23.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.204.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) - '@sentry/opentelemetry': 10.23.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) - import-in-the-middle: 1.14.2 + '@prisma/instrumentation': 6.19.0(@opentelemetry/api@1.9.0) + '@sentry/core': 10.27.0 + '@sentry/node-core': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) + '@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) + import-in-the-middle: 2.0.0 minimatch: 9.0.5 transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@10.23.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)': + '@sentry/opentelemetry@10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 - '@sentry/core': 10.23.0 + '@sentry/core': 10.27.0 - '@sentry/profiling-node@10.23.0': + '@sentry/profiling-node@10.27.0': dependencies: '@sentry-internal/node-cpu-profiler': 2.2.0 - '@sentry/core': 10.23.0 - '@sentry/node': 10.23.0 + '@sentry/core': 10.27.0 + '@sentry/node': 10.27.0 transitivePeerDependencies: - supports-color - '@sentry/react@10.23.0(react@19.2.0)': + '@sentry/react@10.27.0(react@19.2.0)': dependencies: - '@sentry/browser': 10.23.0 - '@sentry/core': 10.23.0 + '@sentry/browser': 10.27.0 + '@sentry/core': 10.27.0 hoist-non-react-statics: 3.3.2 react: 19.2.0 @@ -12252,9 +12209,9 @@ snapshots: dependencies: react: 19.2.0 - '@sentry/webpack-plugin@4.4.0(encoding@0.1.13)(webpack@5.99.6(esbuild@0.25.10))': + '@sentry/webpack-plugin@4.6.1(encoding@0.1.13)(webpack@5.99.6(esbuild@0.25.10))': dependencies: - '@sentry/bundler-plugin-core': 4.4.0(encoding@0.1.13) + '@sentry/bundler-plugin-core': 4.6.1(encoding@0.1.13) unplugin: 1.0.1 uuid: 9.0.1 webpack: 5.99.6(esbuild@0.25.10) @@ -12338,10 +12295,10 @@ snapshots: '@tanstack/devtools-event-client@0.3.4': {} - '@tanstack/eslint-plugin-query@5.83.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@tanstack/eslint-plugin-query@5.83.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -12669,9 +12626,9 @@ snapshots: '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.15.5 + '@types/pg': 8.15.6 - '@types/pg@8.15.5': + '@types/pg@8.15.6': dependencies: '@types/node': 22.17.1 pg-protocol: 1.9.5 @@ -12754,8 +12711,6 @@ snapshots: '@types/node': 22.17.1 '@types/send': 0.17.4 - '@types/shimmer@1.2.0': {} - '@types/sockjs@0.3.36': dependencies: '@types/node': 22.17.1 @@ -12793,15 +12748,15 @@ snapshots: dependencies: '@types/yargs-parser': 15.0.0 - '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/type-utils': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.39.0 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -12810,22 +12765,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/experimental-utils@5.62.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/experimental-utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.39.0 '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.39.0 debug: 4.4.1 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -12858,13 +12813,13 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) debug: 4.4.1 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -12920,39 +12875,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-scope: 5.1.1 semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@8.26.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/utils@8.26.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.26.0 '@typescript-eslint/types': 8.26.0 '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': + '@typescript-eslint/utils@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.39.0 '@typescript-eslint/types': 8.39.0 '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -14327,9 +14282,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)): + eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.6.1)): dependencies: - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node@0.3.9: dependencies: @@ -14339,26 +14294,26 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 enhanced-resolve: 5.18.1 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) get-tsconfig: 4.10.0 is-bun-module: 1.3.0 stable-hash: 0.0.4 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-mdx@3.6.2(eslint@9.34.0(jiti@2.5.1)): + eslint-mdx@3.6.2(eslint@9.34.0(jiti@2.6.1)): dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) espree: 10.4.0 estree-util-visit: 2.0.0 remark-mdx: 3.1.0 @@ -14374,34 +14329,34 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-boundaries@5.0.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-boundaries@5.0.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: chalk: 4.1.2 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) micromatch: 4.0.8 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -14409,7 +14364,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14418,9 +14373,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.5.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14432,35 +14387,35 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest-dom@5.5.0(@testing-library/dom@10.4.0)(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-jest-dom@5.5.0(@testing-library/dom@10.4.0)(eslint@9.34.0(jiti@2.6.1)): dependencies: '@babel/runtime': 7.27.6 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) requireindex: 1.2.0 optionalDependencies: '@testing-library/dom': 10.4.0 - eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))(typescript@5.9.2): + eslint-plugin-jest@29.0.1(@typescript-eslint/eslint-plugin@8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(jest@30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)))(typescript@5.9.2): dependencies: - '@typescript-eslint/utils': 8.26.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 8.26.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) jest: 30.0.4(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.2)) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-mdx@3.6.2(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-mdx@3.6.2(eslint@9.34.0(jiti@2.6.1)): dependencies: - eslint: 9.34.0(jiti@2.5.1) - eslint-mdx: 3.6.2(eslint@9.34.0(jiti@2.5.1)) + eslint: 9.34.0(jiti@2.6.1) + eslint-mdx: 3.6.2(eslint@9.34.0(jiti@2.6.1)) mdast-util-from-markdown: 2.0.2 mdast-util-mdx: 3.0.0 micromark-extension-mdxjs: 3.0.0 @@ -14477,25 +14432,25 @@ snapshots: eslint-plugin-no-relative-import-paths@1.6.1: {} - eslint-plugin-react-hooks@6.1.0(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-react-hooks@6.1.0(eslint@9.34.0(jiti@2.6.1)): dependencies: '@babel/core': 7.28.0 '@babel/parser': 7.28.0 '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.0) - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 3.25.76 zod-validation-error: 3.4.0(zod@3.25.76) transitivePeerDependencies: - supports-color - eslint-plugin-react-you-might-not-need-an-effect@0.5.3(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-react-you-might-not-need-an-effect@0.5.3(eslint@9.34.0(jiti@2.6.1)): dependencies: - eslint: 9.34.0(jiti@2.5.1) - eslint-utils: 3.0.0(eslint@9.34.0(jiti@2.5.1)) + eslint: 9.34.0(jiti@2.6.1) + eslint-utils: 3.0.0(eslint@9.34.0(jiti@2.6.1)) globals: 16.3.0 - eslint-plugin-react@7.37.5(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-react@7.37.5(eslint@9.34.0(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -14503,7 +14458,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -14521,34 +14476,34 @@ snapshots: dependencies: requireindex: 1.2.0 - eslint-plugin-testing-library@7.1.1(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): + eslint-plugin-testing-library@7.1.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): dependencies: '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/utils': 8.26.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 8.26.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): + eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): dependencies: - '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) json-schema: 0.4.0 natural-compare-lite: 1.4.0 typescript: 5.9.2 transitivePeerDependencies: - supports-color - eslint-plugin-unicorn@57.0.0(eslint@9.34.0(jiti@2.5.1)): + eslint-plugin-unicorn@57.0.0(eslint@9.34.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.27.1 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.34.0(jiti@2.6.1)) ci-info: 4.2.0 clean-regexp: 1.0.0 core-js-compat: 3.41.0 - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) esquery: 1.6.0 globals: 15.15.0 indent-string: 5.0.0 @@ -14571,9 +14526,9 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-utils@3.0.0(eslint@9.34.0(jiti@2.5.1)): + eslint-utils@3.0.0(eslint@9.34.0(jiti@2.6.1)): dependencies: - eslint: 9.34.0(jiti@2.5.1) + eslint: 9.34.0(jiti@2.6.1) eslint-visitor-keys: 2.1.0 eslint-visitor-keys@2.1.0: {} @@ -14582,9 +14537,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.34.0(jiti@2.5.1): + eslint@9.34.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 @@ -14620,7 +14575,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.5.1 + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -15022,6 +14977,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@10.5.0: + dependencies: + foreground-child: 3.1.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15031,13 +14995,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - glob@9.3.5: - dependencies: - fs.realpath: 1.0.0 - minimatch: 8.0.4 - minipass: 4.2.8 - path-scurry: 1.11.1 - global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -15378,7 +15335,7 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@1.14.2: + import-in-the-middle@2.0.0: dependencies: acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) @@ -16078,7 +16035,7 @@ snapshots: - supports-color - ts-node - jiti@2.5.1: {} + jiti@2.6.1: {} js-base64@3.7.7: {} @@ -16103,6 +16060,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0: dependencies: cssstyle: 4.4.0 @@ -16183,23 +16144,22 @@ snapshots: kleur@4.1.5: {} - knip@5.64.0(@types/node@22.15.21)(typescript@5.9.2): + knip@5.71.0(@types/node@22.15.21)(typescript@5.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 '@types/node': 22.15.21 fast-glob: 3.3.3 formatly: 0.3.0 - jiti: 2.5.1 - js-yaml: 4.1.0 + jiti: 2.6.1 + js-yaml: 4.1.1 minimist: 1.2.8 - oxc-resolver: 11.8.2 + oxc-resolver: 11.14.0 picocolors: 1.1.1 picomatch: 4.0.2 - smol-toml: 1.4.2 - strip-json-comments: 5.0.2 + smol-toml: 1.5.2 + strip-json-comments: 5.0.3 typescript: 5.9.2 - zod: 3.25.76 - zod-validation-error: 3.4.0(zod@3.25.76) + zod: 4.1.13 known-css-properties@0.34.0: {} @@ -16827,10 +16787,6 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimatch@8.0.4: - dependencies: - brace-expansion: 2.0.1 - minimatch@9.0.1: dependencies: brace-expansion: 2.0.1 @@ -16841,8 +16797,6 @@ snapshots: minimist@1.2.8: {} - minipass@4.2.8: {} - minipass@7.1.2: {} mitt@3.0.1: {} @@ -16896,8 +16850,6 @@ snapshots: napi-postinstall@0.2.4: {} - napi-postinstall@0.3.3: {} - natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -17100,29 +17052,27 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc': 0.74.0 '@oxc-parser/binding-win32-x64-msvc': 0.74.0 - oxc-resolver@11.8.2: - dependencies: - napi-postinstall: 0.3.3 + oxc-resolver@11.14.0: optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.8.2 - '@oxc-resolver/binding-android-arm64': 11.8.2 - '@oxc-resolver/binding-darwin-arm64': 11.8.2 - '@oxc-resolver/binding-darwin-x64': 11.8.2 - '@oxc-resolver/binding-freebsd-x64': 11.8.2 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.8.2 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.8.2 - '@oxc-resolver/binding-linux-arm64-gnu': 11.8.2 - '@oxc-resolver/binding-linux-arm64-musl': 11.8.2 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.8.2 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.8.2 - '@oxc-resolver/binding-linux-riscv64-musl': 11.8.2 - '@oxc-resolver/binding-linux-s390x-gnu': 11.8.2 - '@oxc-resolver/binding-linux-x64-gnu': 11.8.2 - '@oxc-resolver/binding-linux-x64-musl': 11.8.2 - '@oxc-resolver/binding-wasm32-wasi': 11.8.2 - '@oxc-resolver/binding-win32-arm64-msvc': 11.8.2 - '@oxc-resolver/binding-win32-ia32-msvc': 11.8.2 - '@oxc-resolver/binding-win32-x64-msvc': 11.8.2 + '@oxc-resolver/binding-android-arm-eabi': 11.14.0 + '@oxc-resolver/binding-android-arm64': 11.14.0 + '@oxc-resolver/binding-darwin-arm64': 11.14.0 + '@oxc-resolver/binding-darwin-x64': 11.14.0 + '@oxc-resolver/binding-freebsd-x64': 11.14.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.14.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.14.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.14.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.14.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.14.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.14.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.14.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.14.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.14.0 + '@oxc-resolver/binding-linux-x64-musl': 11.14.0 + '@oxc-resolver/binding-wasm32-wasi': 11.14.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.14.0 + '@oxc-resolver/binding-win32-ia32-msvc': 11.14.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.14.0 p-limit@2.3.0: dependencies: @@ -17816,11 +17766,10 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: + require-in-the-middle@8.0.1: dependencies: debug: 4.4.1 module-details-from-path: 1.0.4 - resolve: 1.22.10 transitivePeerDependencies: - supports-color @@ -18021,8 +17970,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - shimmer@1.2.1: {} - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -18071,7 +18018,7 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smol-toml@1.4.2: {} + smol-toml@1.5.2: {} socket.io-adapter@2.5.5: dependencies: @@ -18295,7 +18242,7 @@ snapshots: strip-json-comments@3.1.1: {} - strip-json-comments@5.0.2: {} + strip-json-comments@5.0.3: {} style-loader@4.0.0(webpack@5.99.6(esbuild@0.25.10)): dependencies: @@ -18607,13 +18554,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): + typescript-eslint@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) - eslint: 9.34.0(jiti@2.5.1) + '@typescript-eslint/utils': 8.39.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -19177,6 +19124,8 @@ snapshots: zod@3.25.76: {} + zod@4.1.13: {} + zrender@6.0.0: dependencies: tslib: 2.3.0 diff --git a/pyproject.toml b/pyproject.toml index 0d25b8fe24da2c..db3660876d80da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -312,7 +312,6 @@ ignore_missing_imports = true # - python3 -m tools.mypy_helpers.find_easiest_modules [[tool.mypy.overrides]] module = [ - "sentry.api.endpoints.organization_events_meta", "sentry.api.endpoints.organization_releases", "sentry.api.paginator", "sentry.db.postgres.base", diff --git a/setup.cfg b/setup.cfg index 4d4429b2a51504..6378cc4ecef412 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sentry -version = 25.12.0.dev0 +version = 25.11.1 description = A realtime logging and aggregation server. long_description = file: README.md long_description_content_type = text/markdown diff --git a/src/sentry/api/bases/organization_events.py b/src/sentry/api/bases/organization_events.py index f3f79669f9e686..d95710ceea72a5 100644 --- a/src/sentry/api/bases/organization_events.py +++ b/src/sentry/api/bases/organization_events.py @@ -215,10 +215,6 @@ def quantize_date_params( ) return results - -class OrganizationEventsV2EndpointBase(OrganizationEventsEndpointBase): - owner = ApiOwner.DATA_BROWSING - def build_cursor_link(self, request: HttpRequest, name: str, cursor: Cursor | None) -> str: # The base API function only uses the last query parameter, but this endpoint # needs all the parameters, particularly for the "field" query param. @@ -776,7 +772,7 @@ def serialize_accuracy_data( return serialized_values -class KeyTransactionBase(OrganizationEventsV2EndpointBase): +class KeyTransactionBase(OrganizationEventsEndpointBase): def has_feature(self, organization: Organization, request: Request) -> bool: return features.has("organizations:performance-view", organization, actor=request.user) diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index af34b014c4632f..43d965c370aead 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -1,3 +1,5 @@ +from typing import cast + from django.conf import settings from rest_framework.request import Request from rest_framework.response import Response @@ -6,6 +8,8 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize +from sentry.models.organization import Organization +from sentry.utils.console_platforms import organization_has_console_platform_access def normalize_symbol_source(key, source): @@ -26,10 +30,36 @@ class BuiltinSymbolSourcesEndpoint(Endpoint): permission_classes = () def get(self, request: Request, **kwargs) -> Response: - sources = [ - normalize_symbol_source(key, source) - for key, source in settings.SENTRY_BUILTIN_SOURCES.items() - ] + platform = request.GET.get("platform") + + # Get organization if organization context is available + organization = None + organization_id_or_slug = kwargs.get("organization_id_or_slug") + if organization_id_or_slug: + try: + if str(organization_id_or_slug).isdecimal(): + organization = Organization.objects.get_from_cache(id=organization_id_or_slug) + else: + organization = Organization.objects.get_from_cache(slug=organization_id_or_slug) + except Organization.DoesNotExist: + pass + + sources = [] + for key, source in settings.SENTRY_BUILTIN_SOURCES.items(): + source_platforms: list[str] | None = cast("list[str] | None", source.get("platforms")) + + # If source has platform restrictions, check if current platform matches + if source_platforms is not None: + if not platform or platform not in source_platforms: + continue + + # Platform matches - now check if organization has access to this console platform + if not organization or not organization_has_console_platform_access( + organization, platform + ): + continue + + sources.append(normalize_symbol_source(key, source)) sources.sort(key=lambda s: s["name"]) return Response(serialize(sources)) diff --git a/src/sentry/api/endpoints/organization_ai_conversations.py b/src/sentry/api/endpoints/organization_ai_conversations.py index 56a22a0ce7c031..1a2052205f99ac 100644 --- a/src/sentry/api/endpoints/organization_ai_conversations.py +++ b/src/sentry/api/endpoints/organization_ai_conversations.py @@ -12,7 +12,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization @@ -52,7 +52,7 @@ def validate_sort(self, value): @region_silo_endpoint -class OrganizationAIConversationsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationAIConversationsEndpoint(OrganizationEventsEndpointBase): """Endpoint for fetching AI agent conversation traces.""" publish_status = { diff --git a/src/sentry/api/endpoints/organization_events.py b/src/sentry/api/endpoints/organization_events.py index 679430efc1bec2..9a5e5f5e495ab8 100644 --- a/src/sentry/api/endpoints/organization_events.py +++ b/src/sentry/api/endpoints/organization_events.py @@ -12,7 +12,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.helpers.error_upsampling import ( is_errors_query_for_error_upsampled_projects, transform_orderby_for_error_upsampling, @@ -85,7 +85,7 @@ class EventsApiResponse(TypedDict): @extend_schema(tags=["Discover"]) @region_silo_endpoint -class OrganizationEventsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PUBLIC, } @@ -164,14 +164,6 @@ def get(self, request: Request, organization: Organization) -> Response: """ Retrieves discover (also known as events) data for a given organization. - **Eventsv2 Deprecation Note**: Users who may be using the `eventsv2` endpoint should update their requests to the `events` endpoint outline in this document. - The `eventsv2` endpoint is not a public endpoint and has no guaranteed availability. If you are not making any API calls to `eventsv2`, you can safely ignore this. - Changes between `eventsv2` and `events` include: - - Field keys in the response now match the keys in the requested `field` param exactly. - - The `meta` object in the response now shows types in the nested `field` object. - - Aside from the url change, there are no changes to the request payload itself. - **Note**: This endpoint is intended to get a table of results, and is not for doing a full export of data sent to Sentry. @@ -536,12 +528,10 @@ def get_rpc_config(): ) elif scoped_dataset == TraceMetrics: # tracemetrics uses aggregate conditions - metric_name, metric_type, metric_unit = get_trace_metric_from_request(request) + metric = get_trace_metric_from_request(request) return TraceMetricsSearchResolverConfig( - metric_name=metric_name, - metric_type=metric_type, - metric_unit=metric_unit, + metric=metric, use_aggregate_conditions=use_aggregate_conditions, auto_fields=True, disable_aggregate_extrapolation=disable_aggregate_extrapolation, diff --git a/src/sentry/api/endpoints/organization_events_facets.py b/src/sentry/api/endpoints/organization_events_facets.py index 235e6c3afbb652..4c9abbaa96d0fc 100644 --- a/src/sentry/api/endpoints/organization_events_facets.py +++ b/src/sentry/api/endpoints/organization_events_facets.py @@ -8,7 +8,7 @@ from sentry import tagstore from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp from sentry.models.organization import Organization @@ -27,7 +27,7 @@ class _KeyTopValues(TypedDict): @region_silo_endpoint -class OrganizationEventsFacetsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsFacetsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_facets_performance.py b/src/sentry/api/endpoints/organization_events_facets_performance.py index 381ce043fc633f..6bfcdb88e61cf8 100644 --- a/src/sentry/api/endpoints/organization_events_facets_performance.py +++ b/src/sentry/api/endpoints/organization_events_facets_performance.py @@ -12,7 +12,7 @@ from sentry import features, tagstore from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization @@ -39,7 +39,7 @@ DEFAULT_TAG_KEY_LIMIT = 5 -class OrganizationEventsFacetsPerformanceEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationEventsFacetsPerformanceEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_has_measurements.py b/src/sentry/api/endpoints/organization_events_has_measurements.py index e80cd126979913..06ed00bda2e1d2 100644 --- a/src/sentry/api/endpoints/organization_events_has_measurements.py +++ b/src/sentry/api/endpoints/organization_events_has_measurements.py @@ -9,7 +9,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.snuba import discover @@ -50,7 +50,7 @@ def validate(self, data): @region_silo_endpoint -class OrganizationEventsHasMeasurementsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsHasMeasurementsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_histogram.py b/src/sentry/api/endpoints/organization_events_histogram.py index 4c8a00e68881ba..e7c65fbc20cd32 100644 --- a/src/sentry/api/endpoints/organization_events_histogram.py +++ b/src/sentry/api/endpoints/organization_events_histogram.py @@ -7,7 +7,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.snuba import discover @@ -44,7 +44,7 @@ def validate_field(self, fields): @region_silo_endpoint -class OrganizationEventsHistogramEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsHistogramEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_meta.py b/src/sentry/api/endpoints/organization_events_meta.py index 24d8380fc15c4a..b8b13bdeb9790d 100644 --- a/src/sentry/api/endpoints/organization_events_meta.py +++ b/src/sentry/api/endpoints/organization_events_meta.py @@ -8,11 +8,7 @@ from sentry import features, options, search from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import ( - NoProjects, - OrganizationEventsEndpointBase, - OrganizationEventsV2EndpointBase, -) +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.event_search import parse_search_query from sentry.api.helpers.environments import get_environment_func from sentry.api.helpers.group_index import build_query_params_from_request @@ -147,13 +143,17 @@ def get(self, request: Request, organization: Organization) -> Response: with handle_query_errors(): with sentry_sdk.start_span(op="discover.endpoint", name="filter_creation"): projects = self.get_projects(request, organization) + # Filter out None values from environments + environments = [e for e in snuba_params.environments if e is not None] query_kwargs = build_query_params_from_request( - request, organization, projects, snuba_params.environments + request, organization, projects, environments ) query_kwargs["limit"] = 5 try: # Need to escape quotes in case some "joker" has a transaction with quotes - transaction_name = UNESCAPED_QUOTE_RE.sub('\\"', lookup_keys["transaction"]) + transaction_name = UNESCAPED_QUOTE_RE.sub( + '\\"', lookup_keys["transaction"] or "" + ) parsed_terms = parse_search_query(f'transaction:"{transaction_name}"') except ParseError: return Response({"detail": "Invalid transaction search"}, status=400) @@ -181,7 +181,7 @@ def get(self, request: Request, organization: Organization) -> Response: @region_silo_endpoint -class OrganizationSpansSamplesEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationSpansSamplesEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } @@ -198,11 +198,13 @@ def get(self, request: Request, organization: Organization) -> Response: with handle_query_errors(): if use_eap: - result = get_eap_span_samples(request, snuba_params, orderby) + result: EAPResponse | EventsResponse = get_eap_span_samples( + request, snuba_params, orderby + ) dataset = Spans else: result = get_span_samples(request, snuba_params, orderby) - dataset = spans_indexed + dataset = spans_indexed # type: ignore[assignment] return Response( self.handle_results_with_meta( @@ -282,9 +284,10 @@ def get_span_samples( span_ids.append(top) if len(span_ids) > 0: - query = f"span_id:[{','.join(span_ids)}] {request.query_params.get('query')}" + user_query = request.query_params.get("query") or "" + query = f"span_id:[{','.join(span_ids)}] {user_query}" else: - query = request.query_params.get("query") + query = request.query_params.get("query") or "" return spans_indexed.query( selected_columns=selected_columns, @@ -320,7 +323,7 @@ def get_eap_span_samples( "trace", ] - query_string = request.query_params.get("query") + query_string = request.query_params.get("query") or "" bounds_query_string = f"{column}:>{lower_bound}ms {column}:<{upper_bound}ms {query_string}" rpc_res = Spans.run_table_query( diff --git a/src/sentry/api/endpoints/organization_events_spans_histogram.py b/src/sentry/api/endpoints/organization_events_spans_histogram.py index 24ba9f85259613..5666cdaae11e87 100644 --- a/src/sentry/api/endpoints/organization_events_spans_histogram.py +++ b/src/sentry/api/endpoints/organization_events_spans_histogram.py @@ -5,7 +5,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.search.events.types import Span @@ -36,7 +36,7 @@ def validate_span(self, span: str) -> Span: @region_silo_endpoint -class OrganizationEventsSpansHistogramEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsSpansHistogramEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_spans_performance.py b/src/sentry/api/endpoints/organization_events_spans_performance.py index 4ecabd837909c4..4354d09181e02a 100644 --- a/src/sentry/api/endpoints/organization_events_spans_performance.py +++ b/src/sentry/api/endpoints/organization_events_spans_performance.py @@ -17,7 +17,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.discover.arithmetic import is_equation, strip_equation @@ -86,7 +86,7 @@ class SpanPerformanceColumn: } -class OrganizationEventsSpansEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationEventsSpansEndpointBase(OrganizationEventsEndpointBase): def get_snuba_params( self, request: Request, diff --git a/src/sentry/api/endpoints/organization_events_stats.py b/src/sentry/api/endpoints/organization_events_stats.py index efa0e7c8550020..c33176e66f0cab 100644 --- a/src/sentry/api/endpoints/organization_events_stats.py +++ b/src/sentry/api/endpoints/organization_events_stats.py @@ -11,7 +11,7 @@ from sentry.analytics.events.agent_monitoring_events import AgentMonitoringQuery from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import OrganizationEventsV2EndpointBase +from sentry.api.bases import OrganizationEventsEndpointBase from sentry.api.helpers.error_upsampling import ( is_errors_query_for_error_upsampled_projects, transform_query_columns_for_error_upsampling, @@ -54,7 +54,7 @@ @region_silo_endpoint -class OrganizationEventsStatsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsStatsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } @@ -247,12 +247,10 @@ def get_rpc_config(): if scoped_dataset == TraceMetrics: # tracemetrics uses aggregate conditions - metric_name, metric_type, metric_unit = get_trace_metric_from_request(request) + metric = get_trace_metric_from_request(request) return TraceMetricsSearchResolverConfig( - metric_name=metric_name, - metric_type=metric_type, - metric_unit=metric_unit, + metric=metric, auto_fields=False, use_aggregate_conditions=True, disable_aggregate_extrapolation=request.GET.get( diff --git a/src/sentry/api/endpoints/organization_events_timeseries.py b/src/sentry/api/endpoints/organization_events_timeseries.py index fe886a579e8725..859d53d900b1c9 100644 --- a/src/sentry/api/endpoints/organization_events_timeseries.py +++ b/src/sentry/api/endpoints/organization_events_timeseries.py @@ -10,7 +10,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.endpoints.organization_events_stats import SENTRY_BACKEND_REFERRERS from sentry.api.endpoints.timeseries import ( EMPTY_STATS_RESPONSE, @@ -72,7 +72,7 @@ def null_zero(value: float) -> float | None: @region_silo_endpoint -class OrganizationEventsTimeseriesEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsTimeseriesEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } @@ -226,11 +226,10 @@ def get_rpc_config(): if dataset == TraceMetrics: # tracemetrics uses aggregate conditions - metric_name, metric_type, metric_unit = get_trace_metric_from_request(request) + metric = get_trace_metric_from_request(request) + return TraceMetricsSearchResolverConfig( - metric_name=metric_name, - metric_type=metric_type, - metric_unit=metric_unit, + metric=metric, auto_fields=False, use_aggregate_conditions=True, disable_aggregate_extrapolation=request.GET.get( diff --git a/src/sentry/api/endpoints/organization_events_trace.py b/src/sentry/api/endpoints/organization_events_trace.py index e7d37956ac81f2..c0a0be941a0953 100644 --- a/src/sentry/api/endpoints/organization_events_trace.py +++ b/src/sentry/api/endpoints/organization_events_trace.py @@ -19,7 +19,7 @@ from sentry import constants, features, options from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.serializers.models.event import EventTag, get_tags_with_meta from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp from sentry.issues.issue_occurrence import IssueOccurrence @@ -734,7 +734,7 @@ def pad_span_id(span: str | None) -> str: return span.rjust(16, "0") -class OrganizationEventsTraceEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationEventsTraceEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } @@ -1448,7 +1448,7 @@ def serialize_with_spans( @region_silo_endpoint -class OrganizationEventsTraceMetaEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsTraceMetaEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_trends.py b/src/sentry/api/endpoints/organization_events_trends.py index c4e396dbde9eea..0236b672fe10aa 100644 --- a/src/sentry/api/endpoints/organization_events_trends.py +++ b/src/sentry/api/endpoints/organization_events_trends.py @@ -13,7 +13,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.event_search import AggregateFilter from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors @@ -80,7 +80,7 @@ def resolve_function( return super().resolve_function(function, match, resolve_only, overwrite_alias) -class OrganizationEventsTrendsEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationEventsTrendsEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_trends_v2.py b/src/sentry/api/endpoints/organization_events_trends_v2.py index cfb6bc5f09319c..72c66a7570e5e1 100644 --- a/src/sentry/api/endpoints/organization_events_trends_v2.py +++ b/src/sentry/api/endpoints/organization_events_trends_v2.py @@ -10,7 +10,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.issue_detection.detectors.utils import escape_transaction @@ -46,7 +46,7 @@ @region_silo_endpoint -class OrganizationEventsNewTrendsStatsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsNewTrendsStatsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_events_vitals.py b/src/sentry/api/endpoints/organization_events_vitals.py index 3d081046ff853f..30245724235636 100644 --- a/src/sentry/api/endpoints/organization_events_vitals.py +++ b/src/sentry/api/endpoints/organization_events_vitals.py @@ -6,7 +6,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.search.events.fields import get_function_alias @@ -14,7 +14,7 @@ @region_silo_endpoint -class OrganizationEventsVitalsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsVitalsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py b/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py index 3fdce282d167de..01e7d26ac9b1da 100644 --- a/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py +++ b/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py @@ -12,7 +12,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import OrganizationEventsV2EndpointBase +from sentry.api.bases import OrganizationEventsEndpointBase from sentry.models.organization import Organization from sentry.search.events import fields from sentry.search.events.types import SnubaParams @@ -52,7 +52,7 @@ class StatsQualityEstimation(Enum): @region_silo_endpoint -class OrganizationOnDemandMetricsEstimationStatsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationOnDemandMetricsEstimationStatsEndpoint(OrganizationEventsEndpointBase): """Gets the estimated volume of an organization's metric events.""" publish_status = { diff --git a/src/sentry/api/endpoints/organization_profiling_functions.py b/src/sentry/api/endpoints/organization_profiling_functions.py index 768f0710f2e5cc..a2482538ef4c2b 100644 --- a/src/sentry/api/endpoints/organization_profiling_functions.py +++ b/src/sentry/api/endpoints/organization_profiling_functions.py @@ -12,7 +12,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.exceptions import InvalidSearchQuery @@ -66,7 +66,7 @@ class FunctionTrendsSerializer(serializers.Serializer): @region_silo_endpoint -class OrganizationProfilingFunctionTrendsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationProfilingFunctionTrendsEndpoint(OrganizationEventsEndpointBase): owner = ApiOwner.PROFILING publish_status = { "GET": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/api/endpoints/organization_profiling_profiles.py b/src/sentry/api/endpoints/organization_profiling_profiles.py index 133e4e1c46fa9b..b8e9aeb0c04494 100644 --- a/src/sentry/api/endpoints/organization_profiling_profiles.py +++ b/src/sentry/api/endpoints/organization_profiling_profiles.py @@ -11,7 +11,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.profiles.flamegraph import FlamegraphExecutor @@ -22,7 +22,7 @@ from sentry.utils.snuba import raw_snql_query -class OrganizationProfilingBaseEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationProfilingBaseEndpoint(OrganizationEventsEndpointBase): owner = ApiOwner.PROFILING publish_status = { "GET": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/api/endpoints/organization_spans_fields.py b/src/sentry/api/endpoints/organization_spans_fields.py index 067527399ccbd0..e9c7ba2940919e 100644 --- a/src/sentry/api/endpoints/organization_spans_fields.py +++ b/src/sentry/api/endpoints/organization_spans_fields.py @@ -17,7 +17,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.event_search import translate_escape_sequences from sentry.api.paginator import ChainPaginator from sentry.api.serializers import serialize @@ -54,7 +54,7 @@ def as_tag_key(name: str, type: Literal["string", "number"]): } -class OrganizationSpansFieldsEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationSpansFieldsEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_spans_fields_stats.py b/src/sentry/api/endpoints/organization_spans_fields_stats.py index 7fac80cacdae77..bfb62837c16f2b 100644 --- a/src/sentry/api/endpoints/organization_spans_fields_stats.py +++ b/src/sentry/api/endpoints/organization_spans_fields_stats.py @@ -12,7 +12,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.models.organization import Organization from sentry.search.eap.resolver import SearchResolver from sentry.search.eap.spans.definitions import SPAN_DEFINITIONS @@ -29,7 +29,7 @@ class OrganizationSpansFieldsStatsEndpointSerializer(serializers.Serializer): @region_silo_endpoint -class OrganizationSpansFieldsStatsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationSpansFieldsStatsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } diff --git a/src/sentry/api/endpoints/organization_trace.py b/src/sentry/api/endpoints/organization_trace.py index ae680f29a3732a..f380459634c71b 100644 --- a/src/sentry/api/endpoints/organization_trace.py +++ b/src/sentry/api/endpoints/organization_trace.py @@ -7,7 +7,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp from sentry.models.organization import Organization @@ -19,7 +19,7 @@ @region_silo_endpoint -class OrganizationTraceEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationTraceEndpoint(OrganizationEventsEndpointBase): """Replaces OrganizationEventsTraceEndpoint""" publish_status = { diff --git a/src/sentry/api/endpoints/organization_trace_item_attributes.py b/src/sentry/api/endpoints/organization_trace_item_attributes.py index 318edcf710a0cc..2c6bbe2b844821 100644 --- a/src/sentry/api/endpoints/organization_trace_item_attributes.py +++ b/src/sentry/api/endpoints/organization_trace_item_attributes.py @@ -19,7 +19,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.endpoints.organization_spans_fields import BaseSpanFieldValuesAutocompletionExecutor from sentry.api.event_search import translate_escape_sequences from sentry.api.paginator import ChainPaginator, GenericOffsetPaginator @@ -100,7 +100,7 @@ def get_result(self, limit, cursor=None): ) -class OrganizationTraceItemAttributesEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationTraceItemAttributesEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py b/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py index 4dfa67000bf5de..47e87ff8617565 100644 --- a/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py +++ b/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py @@ -16,7 +16,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.exceptions import InvalidSearchQuery from sentry.models.organization import Organization from sentry.search.eap.resolver import SearchResolver @@ -34,7 +34,7 @@ @region_silo_endpoint -class OrganizationTraceItemsAttributesRankedEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationTraceItemsAttributesRankedEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_trace_item_stats.py b/src/sentry/api/endpoints/organization_trace_item_stats.py index f89faaa7f33957..47f105947aafaf 100644 --- a/src/sentry/api/endpoints/organization_trace_item_stats.py +++ b/src/sentry/api/endpoints/organization_trace_item_stats.py @@ -7,7 +7,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.models.organization import Organization from sentry.search.eap.constants import SUPPORTED_STATS_TYPES from sentry.search.eap.resolver import SearchResolver @@ -27,7 +27,7 @@ class OrganizationTraceItemsStatsSerializer(serializers.Serializer): @region_silo_endpoint -class OrganizationTraceItemsStatsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationTraceItemsStatsEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_trace_logs.py b/src/sentry/api/endpoints/organization_trace_logs.py index cd51df3664f2e4..6eec1b30882fa4 100644 --- a/src/sentry/api/endpoints/organization_trace_logs.py +++ b/src/sentry/api/endpoints/organization_trace_logs.py @@ -6,7 +6,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp from sentry.models.organization import Organization @@ -21,7 +21,7 @@ @region_silo_endpoint -class OrganizationTraceLogsEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationTraceLogsEndpoint(OrganizationEventsEndpointBase): """Replaces a call to events that isn't possible for team plans because of projects restrictions""" publish_status = { diff --git a/src/sentry/api/endpoints/organization_trace_meta.py b/src/sentry/api/endpoints/organization_trace_meta.py index 37cb7b48cee62f..b326c8655b0252 100644 --- a/src/sentry/api/endpoints/organization_trace_meta.py +++ b/src/sentry/api/endpoints/organization_trace_meta.py @@ -9,7 +9,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.endpoints.organization_events_trace import count_performance_issues from sentry.api.utils import handle_query_errors, update_snuba_params_with_timestamp from sentry.models.organization import Organization @@ -52,7 +52,7 @@ def extract_uptime_count(uptime_result: list[TraceItemTableResponse]) -> int: @region_silo_endpoint -class OrganizationTraceMetaEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationTraceMetaEndpoint(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/api/endpoints/organization_traces.py b/src/sentry/api/endpoints/organization_traces.py index 33286e7df1eff8..6549a5147ab5c9 100644 --- a/src/sentry/api/endpoints/organization_traces.py +++ b/src/sentry/api/endpoints/organization_traces.py @@ -30,7 +30,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.exceptions import InvalidSearchQuery @@ -123,7 +123,7 @@ def handle_span_query_errors() -> Generator[None]: raise InvalidSearchQuery(TIMEOUT_SPAN_ERROR_MESSAGE) -class OrganizationTracesEndpointBase(OrganizationEventsV2EndpointBase): +class OrganizationTracesEndpointBase(OrganizationEventsEndpointBase): publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } diff --git a/src/sentry/api/endpoints/project_transaction_threshold_override.py b/src/sentry/api/endpoints/project_transaction_threshold_override.py index 95bbd472165cc0..c53215dfd8709e 100644 --- a/src/sentry/api/endpoints/project_transaction_threshold_override.py +++ b/src/sentry/api/endpoints/project_transaction_threshold_override.py @@ -7,7 +7,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ProjectTransactionThresholdOverridePermission -from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase +from sentry.api.bases.organization_events import OrganizationEventsEndpointBase from sentry.api.serializers import serialize from sentry.models.organization import Organization from sentry.models.transaction_threshold import ( @@ -57,7 +57,7 @@ def validate(self, data): @region_silo_endpoint -class ProjectTransactionThresholdOverrideEndpoint(OrganizationEventsV2EndpointBase): +class ProjectTransactionThresholdOverrideEndpoint(OrganizationEventsEndpointBase): publish_status = { "DELETE": ApiPublishStatus.PRIVATE, "GET": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index a0adf06a64083a..870332378cc81b 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -1,3 +1,9 @@ +from typing import cast + +from django.conf import settings + +from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT +from sentry.models.organization import Organization from sentry.models.project import Project from sentry.projects.services.project import RpcProject @@ -7,11 +13,69 @@ "unity": ["ios", "microsoft", "android", "nuget", "unity", "nvidia", "ubuntu"], "unreal": ["ios", "microsoft", "android", "nvidia", "ubuntu"], "godot": ["ios", "microsoft", "android", "nuget", "nvidia", "ubuntu"], + "nintendo-switch": ["nintendo"], } -def set_default_symbol_sources(project: Project | RpcProject) -> None: - if project.platform and project.platform in DEFAULT_SYMBOL_SOURCES: - project.update_option( - "sentry:builtin_symbol_sources", DEFAULT_SYMBOL_SOURCES[project.platform] - ) +def set_default_symbol_sources( + project: Project | RpcProject, organization: Organization | None = None +) -> None: + """ + Sets default symbol sources for a project based on its platform. + + For sources with platform restrictions (e.g., console platforms), this function checks + if the organization has access to the required platform before adding the source. + + Args: + project: The project to configure symbol sources for + organization: Optional organization (fetched from project if not provided) + """ + if not project.platform or project.platform not in DEFAULT_SYMBOL_SOURCES: + return + + # Get organization from project if not provided + if organization is None: + if isinstance(project, Project): + organization = project.organization + else: + # For RpcProject, fetch organization by ID + try: + organization = Organization.objects.get_from_cache(id=project.organization_id) + except Organization.DoesNotExist: + # If organization doesn't exist, cannot set defaults + return + + # Get default sources for this platform + source_keys = DEFAULT_SYMBOL_SOURCES[project.platform] + + # Get enabled console platforms once (optimization to avoid repeated DB calls) + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + + # Filter sources based on platform restrictions and organization access + enabled_sources = [] + for source_key in source_keys: + source_config = settings.SENTRY_BUILTIN_SOURCES.get(source_key) + + # If source exists in config, check for platform restrictions + if source_config: + required_platforms: list[str] | None = cast( + "list[str] | None", source_config.get("platforms") + ) + if required_platforms: + # Source is platform-restricted - check if org has access + # Only add source if org has access to at least one of the required platforms + has_access = any( + platform in enabled_console_platforms for platform in required_platforms + ) + if not has_access: + continue + + # Include the source (either it passed platform check or doesn't exist in config) + # Non-existent sources will be filtered out at runtime in sources.py + enabled_sources.append(source_key) + + # Always update the option for recognized platforms, even if empty + # This ensures platform-specific defaults override epoch defaults + project.update_option("sentry:builtin_symbol_sources", enabled_sources) diff --git a/src/sentry/api/serializers/models/groupsearchview.py b/src/sentry/api/serializers/models/groupsearchview.py index b2a2b71e49d16c..93f134c92167c5 100644 --- a/src/sentry/api/serializers/models/groupsearchview.py +++ b/src/sentry/api/serializers/models/groupsearchview.py @@ -54,6 +54,7 @@ def get_attrs(self, item_list, user, **kwargs) -> MutableMapping[Any, Any]: filter={"user_ids": [view.user_id for view in item_list if view.user_id]}, as_user=user, ) + if user is not None } for item in item_list: diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 28fc802388986f..6dae2dcde2ea55 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -195,6 +195,7 @@ def env( SENTRY_HYBRIDCLOUD_DELETIONS_REDIS_CLUSTER = "default" SENTRY_SESSION_STORE_REDIS_CLUSTER = "default" SENTRY_AUTH_IDPMIGRATION_REDIS_CLUSTER = "default" +SENTRY_SNOWFLAKE_REDIS_CLUSTER = "default" # Hosts that are allowed to use system token authentication. # http://en.wikipedia.org/wiki/Reserved_IP_addresses @@ -2128,7 +2129,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: SENTRY_SELF_HOSTED_ERRORS_ONLY = False # only referenced in getsentry to provide the stable beacon version # updated with scripts/bump-version.sh -SELF_HOSTED_STABLE_VERSION = "25.11.0" +SELF_HOSTED_STABLE_VERSION = "25.11.1" # Whether we should look at X-Forwarded-For header or not # when checking REMOTE_ADDR ip addresses diff --git a/src/sentry/core/endpoints/team_projects.py b/src/sentry/core/endpoints/team_projects.py index 43bbca677eea54..1888c56c7493e8 100644 --- a/src/sentry/core/endpoints/team_projects.py +++ b/src/sentry/core/endpoints/team_projects.py @@ -47,7 +47,7 @@ def apply_default_project_settings(organization: Organization, project: Project) set_default_disabled_detectors(project) - set_default_symbol_sources(project) + set_default_symbol_sources(project, organization) # Create project option to turn on ML similarity feature for new EA projects if project_is_seer_eligible(project): diff --git a/src/sentry/dynamic_sampling/rules/base.py b/src/sentry/dynamic_sampling/rules/base.py index 637fe756c3079b..3400d2064ebe94 100644 --- a/src/sentry/dynamic_sampling/rules/base.py +++ b/src/sentry/dynamic_sampling/rules/base.py @@ -1,5 +1,4 @@ import logging -from collections import OrderedDict from datetime import datetime, timedelta, timezone import sentry_sdk @@ -8,7 +7,7 @@ from sentry.constants import TARGET_SAMPLE_RATE_DEFAULT from sentry.db.models import Model from sentry.dynamic_sampling.rules.biases.base import Bias -from sentry.dynamic_sampling.rules.combine import get_relay_biases_combinator +from sentry.dynamic_sampling.rules.combine import get_relay_biases from sentry.dynamic_sampling.rules.utils import PolymorphicRule, RuleType, get_enabled_user_biases from sentry.dynamic_sampling.tasks.helpers.boost_low_volume_projects import ( get_boost_low_volume_projects_sample_rate, @@ -94,7 +93,7 @@ def _get_rules_of_enabled_biases( project: Project, base_sample_rate: float, enabled_biases: set[str], - combined_biases: OrderedDict[RuleType, Bias], + combined_biases: dict[RuleType, Bias], ) -> list[PolymorphicRule]: rules = [] @@ -124,7 +123,7 @@ def generate_rules(project: Project) -> list[PolymorphicRule]: enabled_user_biases = get_enabled_user_biases( project.get_option("sentry:dynamic_sampling_biases", None) ) - combined_biases = get_relay_biases_combinator(organization).get_combined_biases() + combined_biases = get_relay_biases(organization) rules = _get_rules_of_enabled_biases( project, base_sample_rate, enabled_user_biases, combined_biases diff --git a/src/sentry/dynamic_sampling/rules/biases/bias_combinator.py b/src/sentry/dynamic_sampling/rules/biases/bias_combinator.py new file mode 100644 index 00000000000000..64dc9e10cf6234 --- /dev/null +++ b/src/sentry/dynamic_sampling/rules/biases/bias_combinator.py @@ -0,0 +1,16 @@ +from collections.abc import Callable + +from sentry.dynamic_sampling.rules.biases.base import Bias +from sentry.dynamic_sampling.rules.utils import RuleType + + +class OrderedBiasesCombinator: + def __init__(self) -> None: + self.biases: dict[RuleType, Bias] = {} + + def add_if(self, rule_type: RuleType, bias: Bias, block: Callable[[], bool]) -> None: + if block(): + self.add(rule_type, bias) + + def add(self, rule_type: RuleType, bias: Bias) -> None: + self.biases[rule_type] = bias diff --git a/src/sentry/dynamic_sampling/rules/combinators/__init__.py b/src/sentry/dynamic_sampling/rules/combinators/__init__.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/src/sentry/dynamic_sampling/rules/combinators/base.py b/src/sentry/dynamic_sampling/rules/combinators/base.py deleted file mode 100644 index ed9999997c5d6a..00000000000000 --- a/src/sentry/dynamic_sampling/rules/combinators/base.py +++ /dev/null @@ -1,45 +0,0 @@ -from abc import ABC, abstractmethod -from collections import OrderedDict -from collections.abc import Callable - -from sentry.dynamic_sampling.rules.biases.base import Bias -from sentry.dynamic_sampling.rules.utils import RuleType - - -class OrderedBias: - """ - Internal representation of a bias which has an order number that defines the total order between other ordered - biases. - """ - - def __init__(self, bias: Bias, order_number: float): - self.bias = bias - self.order_number = order_number - - -class BiasesCombinator(ABC): - """ - Base class representing a way to define total order between biases. - - The need of this class arises as there is the need to be explicit w.r.t to the ordering semantics of the biases. - """ - - def __init__(self) -> None: - self.biases: dict[RuleType, OrderedBias] = {} - - def add_if(self, rule_type: RuleType, bias: Bias, block: Callable[[], bool]) -> None: - if block(): - self.add(rule_type, bias) - - def add(self, rule_type: RuleType, bias: Bias) -> None: - # We assign to this bias an order discriminant, which can be leveraged by the get_combined_biases to - # return an ordered dictionary following a defined total order. - self.biases[rule_type] = OrderedBias(bias, self.get_next_order_number()) - - @abstractmethod - def get_next_order_number(self) -> int: - raise NotImplementedError - - @abstractmethod - def get_combined_biases(self) -> OrderedDict[RuleType, Bias]: - raise NotImplementedError diff --git a/src/sentry/dynamic_sampling/rules/combinators/ordered_combinator.py b/src/sentry/dynamic_sampling/rules/combinators/ordered_combinator.py deleted file mode 100644 index 0ef507988390ca..00000000000000 --- a/src/sentry/dynamic_sampling/rules/combinators/ordered_combinator.py +++ /dev/null @@ -1,22 +0,0 @@ -import collections -from collections import OrderedDict - -from sentry.dynamic_sampling.rules.biases.base import Bias -from sentry.dynamic_sampling.rules.combinators.base import BiasesCombinator -from sentry.dynamic_sampling.rules.utils import RuleType - - -class OrderedBiasesCombinator(BiasesCombinator): - def __init__(self) -> None: - super().__init__() - self.order_discriminant = 0 - - def get_next_order_number(self) -> int: - order_discriminant = self.order_discriminant - self.order_discriminant += 1 - return order_discriminant - - def get_combined_biases(self) -> OrderedDict[RuleType, Bias]: - ordered_biases = list(sorted(self.biases.items(), key=lambda elem: elem[1].order_number)) - biases = map(lambda elem: (elem[0], elem[1].bias), ordered_biases) - return collections.OrderedDict(biases) diff --git a/src/sentry/dynamic_sampling/rules/combine.py b/src/sentry/dynamic_sampling/rules/combine.py index a60a0ed8386717..8f7bcf69e2c780 100644 --- a/src/sentry/dynamic_sampling/rules/combine.py +++ b/src/sentry/dynamic_sampling/rules/combine.py @@ -1,4 +1,6 @@ from sentry import features +from sentry.dynamic_sampling.rules.biases.base import Bias +from sentry.dynamic_sampling.rules.biases.bias_combinator import OrderedBiasesCombinator from sentry.dynamic_sampling.rules.biases.boost_environments_bias import BoostEnvironmentsBias from sentry.dynamic_sampling.rules.biases.boost_latest_releases_bias import BoostLatestReleasesBias from sentry.dynamic_sampling.rules.biases.boost_low_volume_projects_bias import ( @@ -15,13 +17,11 @@ ) from sentry.dynamic_sampling.rules.biases.minimum_sample_rate_bias import MinimumSampleRateBias from sentry.dynamic_sampling.rules.biases.recalibration_bias import RecalibrationBias -from sentry.dynamic_sampling.rules.combinators.base import BiasesCombinator -from sentry.dynamic_sampling.rules.combinators.ordered_combinator import OrderedBiasesCombinator from sentry.dynamic_sampling.rules.utils import RuleType from sentry.models.organization import Organization -def get_relay_biases_combinator(organization: Organization) -> BiasesCombinator: +def get_relay_biases(organization: Organization) -> dict[RuleType, Bias]: is_health_checks_trace_based = features.has( "organizations:ds-health-checks-trace-based", organization, actor=None ) @@ -55,4 +55,4 @@ def get_relay_biases_combinator(organization: Organization) -> BiasesCombinator: ) default_combinator.add(RuleType.BOOST_LOW_VOLUME_PROJECTS_RULE, BoostLowVolumeProjectsBias()) - return default_combinator + return default_combinator.biases diff --git a/src/sentry/explore/translation/alerts_translation.py b/src/sentry/explore/translation/alerts_translation.py index 03790680e2fc1d..d3534c577d3b3a 100644 --- a/src/sentry/explore/translation/alerts_translation.py +++ b/src/sentry/explore/translation/alerts_translation.py @@ -8,12 +8,18 @@ from sentry.incidents.models.alert_rule import AlertRuleDetectionType from sentry.incidents.subscription_processor import MetricIssueDetectorConfig from sentry.incidents.utils.types import DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION +from sentry.search.events.fields import parse_function from sentry.seer.anomaly_detection.store_data import SeerMethod from sentry.seer.anomaly_detection.store_data_workflow_engine import ( handle_send_historical_data_to_seer, ) from sentry.snuba.dataset import Dataset -from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType +from sentry.snuba.models import ( + ExtrapolationMode, + QuerySubscription, + SnubaQuery, + SnubaQueryEventType, +) from sentry.snuba.tasks import update_subscription_in_snuba from sentry.utils.db import atomic_transaction from sentry.workflow_engine.models.data_condition import DataCondition @@ -21,6 +27,14 @@ logger = logging.getLogger(__name__) +COUNT_BASED_ALERT_AGGREAGTES = [ + "count", + "failure_count", + "sum", + "count_if", + "count_unique", +] + def snapshot_snuba_query(snuba_query: SnubaQuery): if snuba_query.dataset in [Dataset.PerformanceMetrics.value, Dataset.Transactions.value]: @@ -88,6 +102,15 @@ def translate_detector_and_update_subscription_in_snuba(snuba_query: SnubaQuery) snuba_query.query = translated_query snuba_query.dataset = Dataset.EventsAnalyticsPlatform.value + function_name, _, _ = parse_function(old_aggregate) + if function_name in COUNT_BASED_ALERT_AGGREAGTES: + if snapshot["dataset"] == Dataset.PerformanceMetrics.value: + snuba_query.extrapolation_mode = ExtrapolationMode.SERVER_WEIGHTED.value + elif snapshot["dataset"] == Dataset.Transactions.value: + snuba_query.extrapolation_mode = ExtrapolationMode.NONE.value + else: + snuba_query.extrapolation_mode = ExtrapolationMode.CLIENT_AND_SERVER_WEIGHTED.value + with atomic_transaction( using=( router.db_for_write(SnubaQuery), diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 38c5c15e14f32b..ad8721765a17cd 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -100,6 +100,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:dashboards-edit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True, default=True) # Enables global filters for dashboards manager.add("organizations:dashboards-global-filters", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enables favourite and duplicate controls for prebuilt dashboards + manager.add("organizations:dashboards-prebuilt-controls", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enables import/export functionality for dashboards manager.add("organizations:dashboards-import", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable metrics enhanced performance in dashboards @@ -321,6 +323,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:preprod-build-distribution", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable preprod frontend routes manager.add("organizations:preprod-frontend-routes", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable preprod issue reporting + manager.add("organizations:preprod-issues", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enables PR page manager.add("organizations:pr-page", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enables the playstation ingestion in relay @@ -670,7 +674,7 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("projects:plugins", ProjectPluginFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=True) # Enables experimental span v2 processing in Relay. manager.add("projects:span-v2-experimental-processing", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Enbale Triage signals V0 for AI powered issue classifiaction in sentry + # Enable Triage signals V0 for AI powered issue classification in sentry manager.add("projects:triage-signals-v0", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("projects:profiling-ingest-unsampled-profiles", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) diff --git a/src/sentry/incidents/endpoints/serializers/alert_rule.py b/src/sentry/incidents/endpoints/serializers/alert_rule.py index d4a604060baeb6..386f28603082a0 100644 --- a/src/sentry/incidents/endpoints/serializers/alert_rule.py +++ b/src/sentry/incidents/endpoints/serializers/alert_rule.py @@ -377,7 +377,9 @@ def get_attrs( serialized_alert_rules = serialize(alert_rules, user=user) serialized_alert_rule_map_by_id = { - serialized_alert["id"]: serialized_alert for serialized_alert in serialized_alert_rules + serialized_alert["id"]: serialized_alert + for serialized_alert in serialized_alert_rules + if serialized_alert } serialized_issue_rules = serialize( @@ -386,7 +388,9 @@ def get_attrs( serializer=RuleSerializer(expand=self.expand), ) serialized_issue_rule_map_by_id = { - serialized_rule["id"]: serialized_rule for serialized_rule in serialized_issue_rules + serialized_rule["id"]: serialized_rule + for serialized_rule in serialized_issue_rules + if serialized_rule } uptime_detectors = [ diff --git a/src/sentry/integrations/slack/webhooks/command.py b/src/sentry/integrations/slack/webhooks/command.py index 548ecbed99263a..d5a3b629b5f4b0 100644 --- a/src/sentry/integrations/slack/webhooks/command.py +++ b/src/sentry/integrations/slack/webhooks/command.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from collections.abc import Iterable from rest_framework import status from rest_framework.request import Request @@ -9,7 +10,6 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.helpers.teams import is_team_admin from sentry.integrations.models.external_actor import ExternalActor from sentry.integrations.slack.message_builder.disconnected import SlackDisconnectedMessageBuilder from sentry.integrations.slack.requests.base import SlackDMRequest, SlackRequestError @@ -18,8 +18,8 @@ from sentry.integrations.slack.views.link_team import build_team_linking_url from sentry.integrations.slack.views.unlink_team import build_team_unlinking_url from sentry.integrations.types import ExternalProviders -from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember +from sentry.models.organizationmemberteam import OrganizationMemberTeam _logger = logging.getLogger("sentry.integration.slack.bot-commands") @@ -44,15 +44,27 @@ NO_CHANNEL_ID_MESSAGE = "Could not identify the Slack channel ID. Please try again." -def is_team_linked_to_channel(organization: Organization, slack_request: SlackDMRequest) -> bool: - """Check if a Slack channel already has a team linked to it""" - return ExternalActor.objects.filter( - organization_id=organization.id, - integration_id=slack_request.integration.id, - provider=ExternalProviders.SLACK.value, - external_name=slack_request.channel_name, - external_id=slack_request.channel_id, - ).exists() +def get_orgs_with_teams_linked_to_channel( + organization_ids: list[int], slack_request: SlackDMRequest +) -> set[int]: + """Get the organizations with teams linked to a Slack channel""" + return set( + ExternalActor.objects.filter( + organization_id__in=organization_ids, + integration_id=slack_request.integration.id, + provider=ExternalProviders.SLACK.value, + external_name=slack_request.channel_name, + external_id=slack_request.channel_id, + ).values_list("organization_id", flat=True) + ) + + +def get_team_admin_member_ids(org_members: Iterable[OrganizationMember]) -> set[int]: + return set( + OrganizationMemberTeam.objects.filter( + organizationmember_id__in=[om.id for om in org_members], role="admin" + ).values_list("organizationmember_id", flat=True) + ) @region_silo_endpoint @@ -91,10 +103,17 @@ def link_team(self, slack_request: SlackDMRequest) -> Response: integration, identity_user ) + # Batch check for team admin roles to avoid N+1 queries + team_admin_member_ids = get_team_admin_member_ids(organization_memberships) + has_valid_role = False for organization_membership in organization_memberships: - if is_valid_role(organization_membership) or is_team_admin(organization_membership): + if ( + is_valid_role(organization_membership) + or organization_membership.id in team_admin_member_ids + ): has_valid_role = True + break if not has_valid_role: return self.reply(slack_request, INSUFFICIENT_ROLE_MESSAGE) @@ -128,15 +147,29 @@ def unlink_team(self, slack_request: SlackDMRequest) -> Response: integration, identity_user ) + # Batch check which organizations have teams linked to this channel + linked_org_ids = get_orgs_with_teams_linked_to_channel( + [om.organization_id for om in organization_memberships], slack_request + ) + + if not linked_org_ids: + return self.reply(slack_request, TEAM_NOT_LINKED_MESSAGE) + + # Batch check for team admin roles to avoid N+1 queries + team_admin_member_ids = get_team_admin_member_ids(organization_memberships) + + # Find an organization where user has both a linked team AND sufficient permissions found: OrganizationMember | None = None for organization_membership in organization_memberships: - if is_team_linked_to_channel(organization_membership.organization, slack_request): - found = organization_membership + if organization_membership.organization_id in linked_org_ids: + if ( + is_valid_role(organization_membership) + or organization_membership.id in team_admin_member_ids + ): + found = organization_membership + break if not found: - return self.reply(slack_request, TEAM_NOT_LINKED_MESSAGE) - - if not is_valid_role(found) and not is_team_admin(found): return self.reply(slack_request, INSUFFICIENT_ROLE_MESSAGE) if not slack_request.user_id: diff --git a/src/sentry/integrations/slack/webhooks/event.py b/src/sentry/integrations/slack/webhooks/event.py index 65fac6eaa09801..9b2f4aee3bf1c9 100644 --- a/src/sentry/integrations/slack/webhooks/event.py +++ b/src/sentry/integrations/slack/webhooks/event.py @@ -186,7 +186,6 @@ def _get_unfurlable_links( # Link can't be unfurled if link_type is None or args is None: - lifecycle.record_halt("Unfurlable link", extra={"url": url}) continue if ( diff --git a/src/sentry/issues/endpoints/organization_issue_timeseries.py b/src/sentry/issues/endpoints/organization_issue_timeseries.py index d72522842b7472..db0f2c5603168e 100644 --- a/src/sentry/issues/endpoints/organization_issue_timeseries.py +++ b/src/sentry/issues/endpoints/organization_issue_timeseries.py @@ -290,6 +290,10 @@ def fill_timeseries( interval: timedelta, values: list[Row], ) -> list[Row]: + # remove microseconds + start = start.replace(microsecond=0) + end = end.replace(microsecond=0) + def iter_interval(start: datetime, end: datetime, interval: timedelta) -> Iterator[int]: while start <= end: yield int(start.timestamp() * 1000) diff --git a/src/sentry/lang/native/sources.py b/src/sentry/lang/native/sources.py index b97eee02cf5dcb..fa9dfdbdb4b849 100644 --- a/src/sentry/lang/native/sources.py +++ b/src/sentry/lang/native/sources.py @@ -86,6 +86,7 @@ "filters": FILTERS_SCHEMA, "is_public": {"type": "boolean"}, "has_index": {"type": "boolean"}, + "platforms": {"type": "array", "items": {"type": "string"}}, } APP_STORE_CONNECT_SCHEMA = { diff --git a/src/sentry/migrations/1010_add_organizationcontributors_table.py b/src/sentry/migrations/1010_add_organizationcontributors_table.py new file mode 100644 index 00000000000000..21607490e6f6d3 --- /dev/null +++ b/src/sentry/migrations/1010_add_organizationcontributors_table.py @@ -0,0 +1,75 @@ +# Generated by Django 5.2.8 on 2025-11-21 21:54 + +import django.db.models.deletion +from django.db import migrations, models + +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey +import sentry.db.models.fields.hybrid_cloud_foreign_key +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "1009_add_date_updated_to_organizationmapping"), + ] + + operations = [ + migrations.CreateModel( + name="OrganizationContributors", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ( + "integration_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.Integration", db_index=True, on_delete="CASCADE" + ), + ), + ("external_identifier", models.CharField(db_index=True, max_length=255)), + ("alias", models.CharField(blank=True, max_length=255, null=True)), + ("num_actions", sentry.db.models.fields.bounded.BoundedIntegerField(default=0)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ("date_updated", models.DateTimeField(auto_now=True)), + ( + "organization", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.organization" + ), + ), + ], + options={ + "db_table": "sentry_organizationcontributors", + "indexes": [ + models.Index( + fields=["organization_id", "date_updated"], + name="sentry_oc_org_date_upd_idx", + ) + ], + "constraints": [ + models.UniqueConstraint( + fields=("organization_id", "integration_id", "external_identifier"), + name="sentry_orgcont_unique_org_cont", + ) + ], + }, + ), + ] diff --git a/src/sentry/models/__init__.py b/src/sentry/models/__init__.py index c4219227436d2e..c34615e484f620 100644 --- a/src/sentry/models/__init__.py +++ b/src/sentry/models/__init__.py @@ -69,6 +69,7 @@ from .options import * # NOQA from .organization import * # NOQA from .organizationaccessrequest import * # NOQA +from .organizationcontributors import * # NOQA from .organizationmapping import * # NOQA from .organizationmember import * # NOQA from .organizationmemberinvite import * # NOQA diff --git a/src/sentry/models/organizationcontributors.py b/src/sentry/models/organizationcontributors.py new file mode 100644 index 00000000000000..b328bc5899cee6 --- /dev/null +++ b/src/sentry/models/organizationcontributors.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from django.db import models + +from sentry.backup.scopes import RelocationScope +from sentry.db.models import BoundedIntegerField, FlexibleForeignKey, region_silo_model +from sentry.db.models.base import DefaultFieldsModel +from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey + + +@region_silo_model +class OrganizationContributors(DefaultFieldsModel): + """ + Tracks external contributors and their activity for an organization. + This model stores information about contributors associated with an + integration for a specific organization, including their external identity + and how many actions they have taken. + """ + + __relocation_scope__ = RelocationScope.Excluded + + organization = FlexibleForeignKey("sentry.Organization", on_delete=models.CASCADE) + + integration_id = HybridCloudForeignKey("sentry.Integration", on_delete="CASCADE") + + external_identifier = models.CharField(max_length=255, db_index=True) + alias = models.CharField(max_length=255, null=True, blank=True) + num_actions = BoundedIntegerField(default=0) + + class Meta: + app_label = "sentry" + db_table = "sentry_organizationcontributors" + constraints = [ + models.UniqueConstraint( + fields=["organization_id", "integration_id", "external_identifier"], + name="sentry_orgcont_unique_org_cont", + ), + ] + indexes = [ + models.Index( + fields=["organization_id", "date_updated"], + name="sentry_oc_org_date_upd_idx", + ), + ] diff --git a/src/sentry/notifications/notification_action/action_handler_registry/email_handler.py b/src/sentry/notifications/notification_action/action_handler_registry/email_handler.py index e50deccc915eae..bdac2869536061 100644 --- a/src/sentry/notifications/notification_action/action_handler_registry/email_handler.py +++ b/src/sentry/notifications/notification_action/action_handler_registry/email_handler.py @@ -47,12 +47,6 @@ class EmailActionHandler(ActionHandler): "description": "The fallthrough type for issue owners email notifications", "enum": [*FallthroughChoiceType], }, - # XXX(CEO): temporarily support this incorrect camel case - "fallthroughType": { - "type": "string", - "description": "The fallthrough type for issue owners email notifications", - "enum": [*FallthroughChoiceType], - }, }, "additionalProperties": False, } diff --git a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py index e10b43007ba297..6eead797cfe531 100644 --- a/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py +++ b/src/sentry/notifications/notification_action/issue_alert_registry/handlers/email_issue_alert_handler.py @@ -51,13 +51,7 @@ def get_additional_fields(cls, action: Action, mapping: ActionFieldMapping) -> d } if target_type == ActionTarget.ISSUE_OWNERS.value: - # XXX(CEO): temporarily handle both fallthroughType and fallthrough_type - action_data = action.data.copy() - if action.data.get("fallthroughType"): - del action_data["fallthroughType"] - action_data["fallthrough_type"] = action.data["fallthroughType"] - - blob = EmailDataBlob(**action_data) + blob = EmailDataBlob(**action.data) final_blob[EmailFieldMappingKeys.FALLTHROUGH_TYPE_KEY.value] = blob.fallthrough_type return final_blob diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index d85609f2f58099..7eef7184ff99a8 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -33,9 +33,10 @@ epoch_defaults={1: "4.x", 2: "5.x", 7: "6.x", 8: "7.x", 13: "8.x", 14: "9.x", 15: "10.x"}, ) -# Default symbol sources. The ios source does not exist by default and -# will be skipped later. The microsoft source exists by default and is -# unlikely to be disabled. +# Default symbol sources. The ios source does not exist by default and +# will be skipped later. The microsoft source exists by default and is +# unlikely to be disabled. Platform-specific sources may be added via +# set_default_symbol_sources() when a project is created. register( key="sentry:builtin_symbol_sources", epoch_defaults={ diff --git a/src/sentry/replays/endpoints/organization_replay_count.py b/src/sentry/replays/endpoints/organization_replay_count.py index 41c0ddc4543224..54c64fdd89aacf 100644 --- a/src/sentry/replays/endpoints/organization_replay_count.py +++ b/src/sentry/replays/endpoints/organization_replay_count.py @@ -12,7 +12,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects -from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase +from sentry.api.bases.organization_events import OrganizationEventsEndpointBase from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN from sentry.apidocs.examples.replay_examples import ReplayExamples from sentry.apidocs.parameters import GlobalParams, OrganizationParams, VisibilityParams @@ -37,7 +37,7 @@ class ReplayCountQueryParamsValidator(serializers.Serializer): @region_silo_endpoint @extend_schema(tags=["Replays"]) -class OrganizationReplayCountEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationReplayCountEndpoint(OrganizationEventsEndpointBase): """ Get all the replay ids associated with a set of issues/transactions in discover, then verify that they exist in the replays dataset, and return the count. diff --git a/src/sentry/replays/endpoints/organization_replay_events_meta.py b/src/sentry/replays/endpoints/organization_replay_events_meta.py index e752a7e804c129..8a7fcc4a223af6 100644 --- a/src/sentry/replays/endpoints/organization_replay_events_meta.py +++ b/src/sentry/replays/endpoints/organization_replay_events_meta.py @@ -8,15 +8,14 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase +from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import reformat_timestamp_ms_to_isoformat from sentry.models.organization import Organization @region_silo_endpoint -class OrganizationReplayEventsMetaEndpoint(OrganizationEventsV2EndpointBase): - # TODO: now that cross-project selection is enabled for all plans, we may be able to consolidate this with the generic OrganizationEventsV2Endpoint +class OrganizationReplayEventsMetaEndpoint(OrganizationEventsEndpointBase): """The generic Events endpoints require that the cross-project selection feature be enabled before they return across multiple projects. diff --git a/src/sentry/replays/query.py b/src/sentry/replays/query.py index 4d9ded5737a71d..1f612352a903b6 100644 --- a/src/sentry/replays/query.py +++ b/src/sentry/replays/query.py @@ -145,7 +145,7 @@ def query_replay_id_by_prefix( query = Query( match=Entity("replays"), - select=[Column("replay_id")], + select=[Column("replay_id"), Column("timestamp")], where=[ Condition(Column("project_id"), Op.IN, project_ids), Condition( @@ -162,6 +162,7 @@ def query_replay_id_by_prefix( Condition(Column("timestamp"), Op.GTE, window_start), Condition(Column("timestamp"), Op.LT, window_end), ], + orderby=[OrderBy(Column("timestamp"), Direction.DESC)], granularity=Granularity(3600), limit=Limit(1), ) diff --git a/src/sentry/search/eap/columns.py b/src/sentry/search/eap/columns.py index 8bd27a68ca6f8f..4aaa0f319c810d 100644 --- a/src/sentry/search/eap/columns.py +++ b/src/sentry/search/eap/columns.py @@ -23,11 +23,19 @@ from sentry.exceptions import InvalidSearchQuery from sentry.search.eap import constants from sentry.search.eap.extrapolation_mode import resolve_extrapolation_mode -from sentry.search.eap.types import EAPResponse, MetricType, SearchResolverConfig +from sentry.search.eap.trace_metrics.types import TraceMetric, TraceMetricType +from sentry.search.eap.types import EAPResponse, SearchResolverConfig from sentry.search.events.types import SnubaParams ResolvedArgument: TypeAlias = AttributeKey | str | int | float ResolvedArguments: TypeAlias = list[ResolvedArgument] +ProtoDefinition: TypeAlias = ( + LiteralValue + | AttributeKey + | AttributeAggregation + | AttributeConditionalAggregation + | Column.BinaryFormula +) class ResolverSettings(TypedDict): @@ -54,6 +62,7 @@ class ResolvedColumn: # Indicates this attribute is a secondary alias for the attribute. # It exists for compatibility or convenience reasons and should NOT be preferred. secondary_alias: bool = False + is_aggregate: bool def process_column(self, value: Any) -> Any: """Given the value from results, return a processed value if a processor is defined otherwise return it""" @@ -81,6 +90,12 @@ def proto_type(self) -> AttributeKey.Type.ValueType: else: return constants.TYPE_MAP[self.search_type] + @property + def proto_definition( + self, + ) -> ProtoDefinition: + raise NotImplementedError + @dataclass(frozen=True, kw_only=True) class ResolvedLiteral(ResolvedColumn): @@ -194,9 +209,7 @@ def proto_definition(self) -> Column.BinaryFormula: @dataclass(frozen=True, kw_only=True) class ResolvedTraceMetricFormula(ResolvedFormula): - metric_name: str | None - metric_type: MetricType | None - metric_unit: str | None + trace_metric: TraceMetric | None @dataclass(frozen=True, kw_only=True) @@ -225,10 +238,32 @@ def proto_definition(self) -> AttributeAggregation: @dataclass(frozen=True, kw_only=True) -class ResolvedTraceMetricAggregate(ResolvedAggregate): - metric_name: str | None - metric_type: MetricType | None - metric_unit: str | None +class ResolvedTraceMetricAggregate(ResolvedFunction): + # The internal rpc alias for this column + internal_name: Function.ValueType + extrapolation_mode: ExtrapolationMode.ValueType + # The attribute to conditionally aggregate on + key: AttributeKey + + is_aggregate: bool = field(default=True, init=False) + trace_metric: TraceMetric | None + + @property + def proto_definition(self) -> AttributeAggregation | AttributeConditionalAggregation: + if self.trace_metric is None: + return AttributeAggregation( + aggregate=self.internal_name, + key=self.key, + label=self.public_alias, + extrapolation_mode=self.extrapolation_mode, + ) + return AttributeConditionalAggregation( + aggregate=self.internal_name, + key=self.key, + filter=self.trace_metric.get_filter(), + label=self.public_alias, + extrapolation_mode=self.extrapolation_mode, + ) @dataclass(frozen=True, kw_only=True) @@ -305,17 +340,17 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate: + ) -> ResolvedFunction: raise NotImplementedError() @dataclass(kw_only=True) class AggregateDefinition(FunctionDefinition): + # The type of aggregation (ex. sum, avg) internal_function: Function.ValueType - """ - An optional function that takes in the resolved argument and returns the attribute key to aggregate on. - If not provided, assumes the aggregate is on the first argument. - """ + # An optional function that takes in the resolved argument and returns the + # attribute key to aggregate on. If not provided, assumes the aggregate is + # on the first argument. attribute_resolver: Callable[[ResolvedArgument], AttributeKey] | None = None def resolve( @@ -326,7 +361,7 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedAggregate: + ) -> ResolvedFunction: if len(resolved_arguments) > 1: raise InvalidSearchQuery( f"Aggregates expects exactly 1 argument, got {len(resolved_arguments)}" @@ -367,7 +402,7 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedAggregate: + ) -> ResolvedFunction: if not isinstance(resolved_arguments[0], AttributeKey): raise InvalidSearchQuery( "Trace metric aggregates expect argument 0 to be of type AttributeArgumentDefinition" @@ -377,9 +412,7 @@ def resolve( if self.attribute_resolver is not None: resolved_attribute = self.attribute_resolver(resolved_attribute) - metric_name, metric_type, metric_unit = extract_trace_metric_aggregate_arguments( - resolved_arguments - ) + trace_metric = extract_trace_metric_aggregate_arguments(resolved_arguments) return ResolvedTraceMetricAggregate( public_alias=alias, @@ -390,15 +423,13 @@ def resolve( extrapolation_mode=resolve_extrapolation_mode( search_config, self.extrapolation_mode_override ), - argument=resolved_attribute, - metric_name=metric_name, - metric_type=metric_type, - metric_unit=metric_unit, + key=resolved_attribute, + trace_metric=trace_metric, ) @dataclass(kw_only=True) -class ConditionalAggregateDefinition(FunctionDefinition): +class ConditionalAggregateDefinition(AggregateDefinition): """ The definition of a conditional aggregation, Conditionally aggregates the `key`, if it passes the `filter`. @@ -406,8 +437,6 @@ class ConditionalAggregateDefinition(FunctionDefinition): The `filter` is returned by the `filter_resolver` function which takes in the args from the user and returns a `TraceItemFilter`. """ - # The type of aggregation (ex. sum, avg) - internal_function: Function.ValueType # A function that takes in the resolved argument and returns the condition to filter on and the key to aggregate on aggregate_resolver: Callable[[ResolvedArguments], tuple[AttributeKey, TraceItemFilter]] @@ -419,7 +448,7 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedConditionalAggregate: + ) -> ResolvedFunction: key, aggregate_filter = self.aggregate_resolver(resolved_arguments) return ResolvedConditionalAggregate( public_alias=alias, @@ -455,7 +484,7 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedFormula: + ) -> ResolvedFunction: resolver_settings = ResolverSettings( extrapolation_mode=resolve_extrapolation_mode( search_config, self.extrapolation_mode_override @@ -488,7 +517,7 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, - ) -> ResolvedFormula: + ) -> ResolvedFunction: resolver_settings = ResolverSettings( extrapolation_mode=resolve_extrapolation_mode( search_config, self.extrapolation_mode_override @@ -498,9 +527,7 @@ def resolve( search_config=search_config, ) - metric_name, metric_type, metric_unit = extract_trace_metric_aggregate_arguments( - resolved_arguments - ) + trace_metric = extract_trace_metric_aggregate_arguments(resolved_arguments) return ResolvedTraceMetricFormula( public_alias=alias, @@ -509,9 +536,7 @@ def resolve( is_aggregate=self.is_aggregate, internal_type=self.internal_type, processor=self.processor, - metric_name=metric_name, - metric_type=metric_type, - metric_unit=metric_unit, + trace_metric=trace_metric, ) @@ -575,21 +600,9 @@ def project_term_resolver( return int(raw_value) -# Any of the resolved attributes, mostly to clean typing up so there's not this giant list all over the code -AnyResolved = ( - ResolvedAttribute - | ResolvedAggregate - | ResolvedConditionalAggregate - | ResolvedFormula - | ResolvedEquation - | ResolvedLiteral -) - - @dataclass(frozen=True) class ColumnDefinitions: aggregates: dict[str, AggregateDefinition] - conditional_aggregates: dict[str, ConditionalAggregateDefinition] formulas: dict[str, FormulaDefinition] columns: dict[str, ResolvedAttribute] contexts: dict[str, VirtualColumnDefinition] @@ -644,25 +657,21 @@ def validate_trace_metric_aggregate_arguments( def extract_trace_metric_aggregate_arguments( resolved_arguments: ResolvedArguments, -) -> tuple[str | None, MetricType | None, str | None]: - metric_name = None - metric_type = None - metric_unit = None - +) -> TraceMetric | None: if all( isinstance(resolved_argument, str) and resolved_argument != "" for resolved_argument in resolved_arguments[1:] ): # a metric was passed - metric_name = cast(str, resolved_arguments[1]) - metric_type = cast(MetricType, resolved_arguments[2]) - metric_unit = None if resolved_arguments[3] == "-" else cast(str, resolved_arguments[3]) + return TraceMetric( + metric_name=cast(str, resolved_arguments[1]), + metric_type=cast(TraceMetricType, resolved_arguments[2]), + metric_unit=None if resolved_arguments[3] == "-" else cast(str, resolved_arguments[3]), + ) elif all(resolved_argument == "" for resolved_argument in resolved_arguments[1:]): # no metrics were specified, assume we query all metrics - pass - else: - raise InvalidSearchQuery( - f"Trace metric aggregates expect the full metric to be specified, got name:{resolved_arguments[1]} type:{resolved_arguments[2]} unit:{resolved_arguments[3]}" - ) + return None - return metric_name, metric_type, metric_unit + raise InvalidSearchQuery( + f"Trace metric aggregates expect the full metric to be specified, got name:{resolved_arguments[1]} type:{resolved_arguments[2]} unit:{resolved_arguments[3]}" + ) diff --git a/src/sentry/search/eap/ourlogs/definitions.py b/src/sentry/search/eap/ourlogs/definitions.py index 2fe10eb8366534..32ee75573445aa 100644 --- a/src/sentry/search/eap/ourlogs/definitions.py +++ b/src/sentry/search/eap/ourlogs/definitions.py @@ -11,7 +11,6 @@ OURLOG_DEFINITIONS = ColumnDefinitions( aggregates=LOG_AGGREGATE_DEFINITIONS, - conditional_aggregates={}, formulas={}, columns=OURLOG_ATTRIBUTE_DEFINITIONS, contexts=OURLOG_VIRTUAL_CONTEXTS, diff --git a/src/sentry/search/eap/profile_functions/definitions.py b/src/sentry/search/eap/profile_functions/definitions.py index ca60342183f4f3..ac296a546c040e 100644 --- a/src/sentry/search/eap/profile_functions/definitions.py +++ b/src/sentry/search/eap/profile_functions/definitions.py @@ -9,7 +9,6 @@ PROFILE_FUNCTIONS_DEFINITIONS = ColumnDefinitions( aggregates=PROFILE_FUNCTIONS_AGGREGATE_DEFINITIONS, - conditional_aggregates={}, formulas={}, columns=PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS, contexts=PROFILE_FUNCTIONS_VIRTUAL_CONTEXTS, diff --git a/src/sentry/search/eap/resolver.py b/src/sentry/search/eap/resolver.py index 84f580682882b5..bad24f9be249ba 100644 --- a/src/sentry/search/eap/resolver.py +++ b/src/sentry/search/eap/resolver.py @@ -42,16 +42,13 @@ from sentry.search.eap import constants from sentry.search.eap.columns import ( AggregateDefinition, - AnyResolved, AttributeArgumentDefinition, ColumnDefinitions, - ConditionalAggregateDefinition, FormulaDefinition, - ResolvedAggregate, ResolvedAttribute, - ResolvedConditionalAggregate, + ResolvedColumn, ResolvedEquation, - ResolvedFormula, + ResolvedFunction, ResolvedLiteral, ValueArgumentDefinition, VirtualColumnDefinition, @@ -84,20 +81,18 @@ class SearchResolver: _resolved_function_cache: dict[ str, tuple[ - ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate, + ResolvedFunction, VirtualColumnDefinition | None, ], ] = field(default_factory=dict) def get_function_definition( self, function_name: str - ) -> ConditionalAggregateDefinition | FormulaDefinition | AggregateDefinition: + ) -> FormulaDefinition | AggregateDefinition: if function_name in self.definitions.aggregates: return self.definitions.aggregates[function_name] elif function_name in self.definitions.formulas: return self.definitions.formulas[function_name] - elif function_name in self.definitions.conditional_aggregates: - return self.definitions.conditional_aggregates[function_name] else: raise InvalidSearchQuery(f"Unknown function {function_name}") @@ -790,9 +785,7 @@ def resolve_contexts( @sentry_sdk.trace def resolve_columns(self, selected_columns: list[str], has_aggregates: bool = False) -> tuple[ - list[ - ResolvedAttribute | ResolvedAggregate | ResolvedConditionalAggregate | ResolvedFormula - ], + list[ResolvedAttribute | ResolvedFunction], list[VirtualColumnDefinition | None], ]: """Given a list of columns resolve them and get their context if applicable @@ -832,7 +825,7 @@ def resolve_column( match: Match[str] | None = None, public_alias_override: str | None = None, ) -> tuple[ - ResolvedAttribute | ResolvedAggregate | ResolvedConditionalAggregate | ResolvedFormula, + ResolvedAttribute | ResolvedFunction, VirtualColumnDefinition | None, ]: """Column is either an attribute or an aggregate, this function will determine which it is and call the relevant @@ -938,7 +931,7 @@ def resolve_attribute( @sentry_sdk.trace def resolve_functions(self, columns: list[str]) -> tuple[ - list[ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate], + list[ResolvedFunction], list[VirtualColumnDefinition | None], ]: """Helper function to resolve a list of functions instead of 1 attribute at a time""" @@ -954,10 +947,7 @@ def resolve_function( column: str, match: Match[str] | None = None, public_alias_override: str | None = None, - ) -> tuple[ - ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate, - VirtualColumnDefinition | None, - ]: + ) -> tuple[ResolvedFunction, VirtualColumnDefinition | None]: if match is None: match = fields.is_function(column) if match is None: @@ -1081,7 +1071,7 @@ def resolve_function( return self._resolved_function_cache[alias] def resolve_equations(self, equations: list[str]) -> tuple[ - list[AnyResolved], + list[ResolvedColumn], list[VirtualColumnDefinition], ]: formulas = [] @@ -1093,7 +1083,7 @@ def resolve_equations(self, equations: list[str]) -> tuple[ return formulas, contexts def resolve_equation(self, equation: str) -> tuple[ - AnyResolved, + ResolvedColumn, list[VirtualColumnDefinition], ]: """Resolve an equation creating a ResolvedEquation object, we don't just return a Column.BinaryFormula since @@ -1171,18 +1161,25 @@ def _resolve_operation(self, operation: arithmetic.OperandType) -> tuple[ ) elif isinstance(operation, float): return Column(literal=LiteralValue(val_double=operation)), [] - else: - # Resolve the column, and turn it into a RPC Column so it can be used in a BinaryFormula - col, context = self.resolve_column(operation) - contexts = [context] if context is not None else [] - if isinstance(col, ResolvedAttribute): - return Column(key=col.proto_definition), contexts - elif isinstance(col, ResolvedAggregate): - return Column(aggregation=col.proto_definition), contexts - elif isinstance(col, ResolvedConditionalAggregate): - return Column(conditional_aggregation=col.proto_definition), contexts - elif isinstance(col, ResolvedFormula): - return Column(formula=col.proto_definition), contexts + + # Resolve the column, and turn it into a RPC Column so it can be used in a BinaryFormula + col, context = self.resolve_column(operation) + contexts = [context] if context is not None else [] + proto_definition = col.proto_definition + + if isinstance(proto_definition, AttributeKey): + return Column(key=proto_definition), contexts + + if isinstance(proto_definition, AttributeAggregation): + return Column(aggregation=proto_definition), contexts + + if isinstance(proto_definition, AttributeConditionalAggregation): + return Column(conditional_aggregation=proto_definition), contexts + + if isinstance(proto_definition, Column.BinaryFormula): + return Column(formula=proto_definition), contexts + + raise TypeError(f"Unsupported proto definition type: {type(proto_definition)}") def resolve_dataset_conditions( self, diff --git a/src/sentry/search/eap/rpc_utils.py b/src/sentry/search/eap/rpc_utils.py index 2fb2465d41fc4e..acd604d5f4ea89 100644 --- a/src/sentry/search/eap/rpc_utils.py +++ b/src/sentry/search/eap/rpc_utils.py @@ -1,4 +1,4 @@ -from sentry_protos.snuba.v1.trace_item_filter_pb2 import AndFilter, TraceItemFilter +from sentry_protos.snuba.v1.trace_item_filter_pb2 import AndFilter, OrFilter, TraceItemFilter def and_trace_item_filters( @@ -12,3 +12,16 @@ def and_trace_item_filters( return filters[0] return TraceItemFilter(and_filter=AndFilter(filters=filters)) + + +def or_trace_item_filters( + *trace_item_filters: TraceItemFilter | None, +) -> TraceItemFilter | None: + filters: list[TraceItemFilter] = [f for f in trace_item_filters if f is not None] + if not filters: + return None + + if len(filters) == 1: + return filters[0] + + return TraceItemFilter(or_filter=OrFilter(filters=filters)) diff --git a/src/sentry/search/eap/spans/aggregates.py b/src/sentry/search/eap/spans/aggregates.py index 59de1ff0ebe6f7..4a9eb1b1cd32dd 100644 --- a/src/sentry/search/eap/spans/aggregates.py +++ b/src/sentry/search/eap/spans/aggregates.py @@ -180,7 +180,7 @@ def resolve_bounded_sample(args: ResolvedArguments) -> tuple[AttributeKey, Trace return (attribute, filter) -SPAN_CONDITIONAL_AGGREGATE_DEFINITIONS = { +SPAN_AGGREGATE_DEFINITIONS = { "count_op": ConditionalAggregateDefinition( internal_function=Function.FUNCTION_COUNT, default_search_type="integer", @@ -455,9 +455,6 @@ def resolve_bounded_sample(args: ResolvedArguments) -> tuple[AttributeKey, Trace processor=lambda x: x > 0, extrapolation_mode_override=ExtrapolationMode.EXTRAPOLATION_MODE_NONE, ), -} - -SPAN_AGGREGATE_DEFINITIONS = { "sum": AggregateDefinition( internal_function=Function.FUNCTION_SUM, default_search_type="duration", diff --git a/src/sentry/search/eap/spans/definitions.py b/src/sentry/search/eap/spans/definitions.py index 03c6eca4f040e4..6207a00e89f928 100644 --- a/src/sentry/search/eap/spans/definitions.py +++ b/src/sentry/search/eap/spans/definitions.py @@ -1,17 +1,13 @@ from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType from sentry.search.eap.columns import ColumnDefinitions -from sentry.search.eap.spans.aggregates import ( - SPAN_AGGREGATE_DEFINITIONS, - SPAN_CONDITIONAL_AGGREGATE_DEFINITIONS, -) +from sentry.search.eap.spans.aggregates import SPAN_AGGREGATE_DEFINITIONS from sentry.search.eap.spans.attributes import SPAN_ATTRIBUTE_DEFINITIONS, SPAN_VIRTUAL_CONTEXTS from sentry.search.eap.spans.filter_aliases import SPAN_FILTER_ALIAS_DEFINITIONS from sentry.search.eap.spans.formulas import SPAN_FORMULA_DEFINITIONS SPAN_DEFINITIONS = ColumnDefinitions( aggregates=SPAN_AGGREGATE_DEFINITIONS, - conditional_aggregates=SPAN_CONDITIONAL_AGGREGATE_DEFINITIONS, formulas=SPAN_FORMULA_DEFINITIONS, columns=SPAN_ATTRIBUTE_DEFINITIONS, contexts=SPAN_VIRTUAL_CONTEXTS, diff --git a/src/sentry/search/eap/trace_metrics/config.py b/src/sentry/search/eap/trace_metrics/config.py index ceec30f6e90289..e96cf39a7c34f9 100644 --- a/src/sentry/search/eap/trace_metrics/config.py +++ b/src/sentry/search/eap/trace_metrics/config.py @@ -2,32 +2,20 @@ from typing import cast from rest_framework.request import Request -from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue -from sentry_protos.snuba.v1.trace_item_filter_pb2 import ( - AndFilter, - ComparisonFilter, - TraceItemFilter, -) +from sentry_protos.snuba.v1.trace_item_filter_pb2 import TraceItemFilter from sentry.exceptions import InvalidSearchQuery from sentry.search.eap.columns import ResolvedTraceMetricAggregate, ResolvedTraceMetricFormula from sentry.search.eap.resolver import SearchResolver -from sentry.search.eap.types import MetricType, SearchResolverConfig +from sentry.search.eap.rpc_utils import or_trace_item_filters +from sentry.search.eap.trace_metrics.types import TraceMetric, TraceMetricType +from sentry.search.eap.types import SearchResolverConfig from sentry.search.events import fields -@dataclass(frozen=True, kw_only=True) -class Metric: - metric_name: str - metric_type: MetricType - metric_unit: str | None - - @dataclass(frozen=True, kw_only=True) class TraceMetricsSearchResolverConfig(SearchResolverConfig): - metric_name: str | None - metric_type: MetricType | None - metric_unit: str | None + metric: TraceMetric | None def extra_conditions( self, @@ -53,7 +41,8 @@ def _extra_conditions_from_columns( selected_columns: list[str] | None, equations: list[str] | None, ) -> TraceItemFilter | None: - selected_metrics: set[Metric] = set() + aggregate_all_metrics = False + selected_metrics: set[TraceMetric] = set() if selected_columns: stripped_columns = [column.strip() for column in selected_columns] @@ -69,103 +58,59 @@ def _extra_conditions_from_columns( ) and not isinstance(resolved_function, ResolvedTraceMetricFormula): continue - if not resolved_function.metric_name or not resolved_function.metric_type: + if resolved_function.trace_metric is None: + # found an aggregation across all metrics, not just 1 + aggregate_all_metrics = True continue - metric = Metric( - metric_name=resolved_function.metric_name, - metric_type=resolved_function.metric_type, - metric_unit=resolved_function.metric_unit, - ) - selected_metrics.add(metric) + selected_metrics.add(resolved_function.trace_metric) + + if equations: + raise InvalidSearchQuery("Cannot support equations on trace metrics yet") + # no selected metrics, no filter needed if not selected_metrics: return None - if len(selected_metrics) > 1: - raise InvalidSearchQuery("Cannot aggregate multiple metrics in 1 query.") - - selected_metric = selected_metrics.pop() + # check if there are any aggregations across all metrics mixed with + # aggregations for a single metric as this is not permitted + if aggregate_all_metrics and selected_metrics: + raise InvalidSearchQuery( + "Cannot aggregate all metrics and singlular metrics in the same query." + ) - return get_metric_filter(search_resolver, selected_metric) + # at this point we only have selected metrics remaining + filters = [metric.get_filter() for metric in selected_metrics] + return or_trace_item_filters(*filters) def _extra_conditions_from_metric( self, search_resolver: SearchResolver, ) -> TraceItemFilter | None: - if not self.metric_name or not self.metric_type: + if self.metric is None: return None + return self.metric.get_filter() - metric = Metric( - metric_name=self.metric_name, - metric_type=self.metric_type, - metric_unit=self.metric_unit, - ) - - return get_metric_filter(search_resolver, metric) - - -def get_metric_filter( - search_resolver: SearchResolver, - metric: Metric, -) -> TraceItemFilter: - metric_name, _ = search_resolver.resolve_column("metric.name") - if not isinstance(metric_name.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.name") - - metric_type, _ = search_resolver.resolve_column("metric.type") - if not isinstance(metric_type.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.type") - - filters = [ - TraceItemFilter( - comparison_filter=ComparisonFilter( - key=metric_name.proto_definition, - op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=metric.metric_name), - ) - ), - TraceItemFilter( - comparison_filter=ComparisonFilter( - key=metric_type.proto_definition, - op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=metric.metric_type), - ) - ), - ] - - if metric.metric_unit: - metric_unit, _ = search_resolver.resolve_column("metric.unit") - if not isinstance(metric_unit.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.unit") - filters.append( - TraceItemFilter( - comparison_filter=ComparisonFilter( - key=metric_unit.proto_definition, - op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=metric.metric_unit), - ) - ) - ) - - return TraceItemFilter(and_filter=AndFilter(filters=filters)) - -ALLOWED_METRIC_TYPES: list[MetricType] = ["counter", "gauge", "distribution"] +ALLOWED_METRIC_TYPES: list[TraceMetricType] = ["counter", "gauge", "distribution"] def get_trace_metric_from_request( request: Request, -) -> tuple[str | None, MetricType | None, str | None]: +) -> TraceMetric | None: metric_name = request.GET.get("metricName") metric_type = request.GET.get("metricType") metric_unit = request.GET.get("metricUnit") if not metric_name: - metric_name = None - if not metric_type: - metric_type = None + return None + if not metric_type or metric_type not in ALLOWED_METRIC_TYPES: + return None if not metric_unit: metric_unit = None - return metric_name, cast(MetricType | None, metric_type), metric_unit + return TraceMetric( + metric_name=metric_name, + metric_type=cast(TraceMetricType, metric_type), + metric_unit=metric_unit, + ) diff --git a/src/sentry/search/eap/trace_metrics/definitions.py b/src/sentry/search/eap/trace_metrics/definitions.py index 01b2ecd5007cff..85389cf9bce670 100644 --- a/src/sentry/search/eap/trace_metrics/definitions.py +++ b/src/sentry/search/eap/trace_metrics/definitions.py @@ -10,7 +10,6 @@ TRACE_METRICS_DEFINITIONS = ColumnDefinitions( aggregates=TRACE_METRICS_AGGREGATE_DEFINITIONS, - conditional_aggregates={}, formulas=TRACE_METRICS_FORMULA_DEFINITIONS, columns=TRACE_METRICS_ATTRIBUTE_DEFINITIONS, contexts=TRACE_METRICS_VIRTUAL_CONTEXTS, diff --git a/src/sentry/search/eap/trace_metrics/formulas.py b/src/sentry/search/eap/trace_metrics/formulas.py index a3584bea7e7c91..8de72543fb1075 100644 --- a/src/sentry/search/eap/trace_metrics/formulas.py +++ b/src/sentry/search/eap/trace_metrics/formulas.py @@ -1,5 +1,6 @@ -from typing import cast - +from sentry_protos.snuba.v1.attribute_conditional_aggregation_pb2 import ( + AttributeConditionalAggregation, +) from sentry_protos.snuba.v1.endpoint_trace_item_table_pb2 import Column from sentry_protos.snuba.v1.formula_pb2 import Literal as LiteralValue from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ( @@ -16,13 +17,14 @@ ResolverSettings, TraceMetricFormulaDefinition, ValueArgumentDefinition, + extract_trace_metric_aggregate_arguments, ) from sentry.search.eap.trace_metrics.config import TraceMetricsSearchResolverConfig from sentry.search.eap.validator import literal_validator def _rate_internal( - divisor: int, metric_type: str, settings: ResolverSettings + divisor: int, args: ResolvedArguments, settings: ResolverSettings ) -> Column.BinaryFormula: """ Calculate rate per X for trace metrics using the value attribute. @@ -40,31 +42,37 @@ def _rate_internal( else settings["snuba_params"].interval ) - metric_type = search_config.metric_type if search_config.metric_type else metric_type - - if metric_type == "counter": - return Column.BinaryFormula( - left=Column( - aggregation=AttributeAggregation( - aggregate=Function.FUNCTION_SUM, - key=AttributeKey(type=AttributeKey.TYPE_DOUBLE, name="sentry.value"), - extrapolation_mode=extrapolation_mode, - ), - ), - op=Column.BinaryFormula.OP_DIVIDE, - right=Column( - literal=LiteralValue(val_double=time_interval / divisor), - ), - ) + trace_metric = search_config.metric or extract_trace_metric_aggregate_arguments(args) - return Column.BinaryFormula( - left=Column( + if trace_metric is None: + left = Column( aggregation=AttributeAggregation( aggregate=Function.FUNCTION_COUNT, key=AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT), extrapolation_mode=extrapolation_mode, - ), - ), + ) + ) + elif trace_metric.metric_type == "counter": + left = Column( + conditional_aggregation=AttributeConditionalAggregation( + aggregate=Function.FUNCTION_SUM, + key=AttributeKey(type=AttributeKey.TYPE_DOUBLE, name="sentry.value"), + filter=trace_metric.get_filter(), + extrapolation_mode=extrapolation_mode, + ) + ) + else: + left = Column( + conditional_aggregation=AttributeConditionalAggregation( + aggregate=Function.FUNCTION_COUNT, + key=AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT), + filter=trace_metric.get_filter(), + extrapolation_mode=extrapolation_mode, + ) + ) + + return Column.BinaryFormula( + left=left, op=Column.BinaryFormula.OP_DIVIDE, right=Column( literal=LiteralValue(val_double=time_interval / divisor), @@ -76,16 +84,14 @@ def per_second(args: ResolvedArguments, settings: ResolverSettings) -> Column.Bi """ Calculate rate per second for trace metrics using the value attribute. """ - metric_type = cast(str, args[2]) if len(args) >= 3 and args[2] else "counter" - return _rate_internal(1, metric_type, settings) + return _rate_internal(1, args, settings) def per_minute(args: ResolvedArguments, settings: ResolverSettings) -> Column.BinaryFormula: """ Calculate rate per minute for trace metrics using the value attribute. """ - metric_type = cast(str, args[2]) if len(args) >= 3 and args[2] else "counter" - return _rate_internal(60, metric_type, settings) + return _rate_internal(60, args, settings) TRACE_METRICS_FORMULA_DEFINITIONS: dict[str, FormulaDefinition] = { diff --git a/src/sentry/search/eap/trace_metrics/types.py b/src/sentry/search/eap/trace_metrics/types.py new file mode 100644 index 00000000000000..6a4bb1cf104da6 --- /dev/null +++ b/src/sentry/search/eap/trace_metrics/types.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass +from typing import Literal + +from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue +from sentry_protos.snuba.v1.trace_item_filter_pb2 import ( + AndFilter, + ComparisonFilter, + TraceItemFilter, +) + +TraceMetricType = Literal["counter", "gauge", "distribution"] + + +@dataclass(frozen=True, kw_only=True) +class TraceMetric: + metric_name: str + metric_type: TraceMetricType + metric_unit: str | None + + def get_filter(self) -> TraceItemFilter: + filters = [ + TraceItemFilter( + comparison_filter=ComparisonFilter( + key=AttributeKey(name="sentry.metric_name", type=AttributeKey.Type.TYPE_STRING), + op=ComparisonFilter.OP_EQUALS, + value=AttributeValue(val_str=self.metric_name), + ) + ), + TraceItemFilter( + comparison_filter=ComparisonFilter( + key=AttributeKey(name="sentry.metric_type", type=AttributeKey.Type.TYPE_STRING), + op=ComparisonFilter.OP_EQUALS, + value=AttributeValue(val_str=self.metric_type), + ) + ), + ] + + if self.metric_unit: + filters.append( + TraceItemFilter( + comparison_filter=ComparisonFilter( + key=AttributeKey( + name="sentry.metric_unit", type=AttributeKey.Type.TYPE_STRING + ), + op=ComparisonFilter.OP_EQUALS, + value=AttributeValue(val_str=self.metric_unit), + ) + ) + ) + + return TraceItemFilter(and_filter=AndFilter(filters=filters)) diff --git a/src/sentry/search/eap/types.py b/src/sentry/search/eap/types.py index b214746e92903d..05d052dd51c27c 100644 --- a/src/sentry/search/eap/types.py +++ b/src/sentry/search/eap/types.py @@ -87,6 +87,3 @@ class AdditionalQueries: span: list[str] | None log: list[str] | None metric: list[str] | None - - -MetricType = Literal["counter", "gauge", "distribution"] diff --git a/src/sentry/search/eap/uptime_results/definitions.py b/src/sentry/search/eap/uptime_results/definitions.py index 2fb6fa159e3e95..666b3235d8b828 100644 --- a/src/sentry/search/eap/uptime_results/definitions.py +++ b/src/sentry/search/eap/uptime_results/definitions.py @@ -5,7 +5,6 @@ UPTIME_RESULT_DEFINITIONS = ColumnDefinitions( aggregates={}, - conditional_aggregates={}, formulas={}, columns=UPTIME_ATTRIBUTE_DEFINITIONS, contexts={}, diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index a7f3b1fab218ab..772b0dcde24519 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -320,9 +320,9 @@ def run_automation( if source == SeerAutomationSource.ISSUE_DETAILS: return - # Check event count for ALERT source with triage-signals-v0 + # Check event count for ALERT source with triage-signals-v0-org if source == SeerAutomationSource.ALERT and features.has( - "projects:triage-signals-v0", group.project + "organizations:triage-signals-v0-org", group.organization ): # Use times_seen_with_pending if available (set by post_process), otherwise fall back times_seen = ( @@ -333,19 +333,24 @@ def run_automation( if times_seen < 10: logger.info( "Triage signals V0: skipping alert automation, event count < 10", - extra={"group_id": group.id, "event_count": times_seen}, + extra={ + "group_id": group.id, + "project_id": group.project.id, + "event_count": times_seen, + }, ) return - # Only log for projects with triage-signals-v0 - if features.has("projects:triage-signals-v0", group.project): + # Only log for orgs with triage-signals-v0-org + if features.has("organizations:triage-signals-v0-org", group.organization): try: times_seen = group.times_seen_with_pending except (AssertionError, AttributeError): times_seen = group.times_seen logger.info( - "Triage signals V0: %s: run_automation called: source=%s, times_seen=%s", + "Triage signals V0: %s: run_automation called: project_id=%s, source=%s, times_seen=%s", group.id, + group.project.id, source.value, times_seen, ) @@ -389,7 +394,7 @@ def run_automation( return stopping_point = None - if features.has("projects:triage-signals-v0", group.project): + if features.has("organizations:triage-signals-v0-org", group.organization): logger.info("Triage signals V0: %s: generating stopping point", group.id) fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score) logger.info("Fixability-based stopping point: %s", fixability_stopping_point) diff --git a/src/sentry/seer/endpoints/organization_events_anomalies.py b/src/sentry/seer/endpoints/organization_events_anomalies.py index 5742b16ce111c7..2a32c606baed05 100644 --- a/src/sentry/seer/endpoints/organization_events_anomalies.py +++ b/src/sentry/seer/endpoints/organization_events_anomalies.py @@ -7,7 +7,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationAlertRulePermission -from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase +from sentry.api.bases.organization_events import OrganizationEventsEndpointBase from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers.base import serialize from sentry.apidocs.constants import ( @@ -27,7 +27,7 @@ @region_silo_endpoint -class OrganizationEventsAnomaliesEndpoint(OrganizationEventsV2EndpointBase): +class OrganizationEventsAnomaliesEndpoint(OrganizationEventsEndpointBase): owner = ApiOwner.ALERTS_NOTIFICATIONS publish_status = { "POST": ApiPublishStatus.EXPERIMENTAL, diff --git a/src/sentry/seer/explorer/tools.py b/src/sentry/seer/explorer/tools.py index aeab785afaf501..f0256f96b0467f 100644 --- a/src/sentry/seer/explorer/tools.py +++ b/src/sentry/seer/explorer/tools.py @@ -89,8 +89,8 @@ def _get_full_trace_id( subquery_result = Spans.run_table_query( params=snuba_params, query_string=f"trace:{short_trace_id}", - selected_columns=["trace"], - orderby=[], + selected_columns=["trace", "timestamp"], + orderby=["-timestamp"], offset=0, limit=1, referrer=Referrer.SEER_RPC, @@ -1017,7 +1017,6 @@ def _make_get_trace_request( - timestamp: ISO 8601 timestamp, Z suffix. - attributes: A dictionary of dictionaries, where the keys are the attribute names. - attributes[name].value: The value of the attribute (primitives only) - - attributes[name].type: The type of the attribute ("str", "int", "double", "bool") """ organization = cast(Organization, resolver.params.organization) projects = list(resolver.params.projects) @@ -1065,43 +1064,41 @@ def _make_get_trace_request( attr_dict: dict[str, dict[str, Any]] = {} for a in item.attributes: r = resolved_attrs_by_internal_name.get(a.key.name) - public_alias = r.public_alias if r else a.key.name - if public_alias == "project_id": # Same internal name, normalize to project.id - public_alias = "project.id" + name = r.public_alias if r else a.key.name + + if name.startswith("sentry._internal"): + continue + + if name == "project_id": # Same internal name, normalize to project.id + name = "project.id" # Note - custom attrs not in the definitions can only be returned as strings or doubles. if a.key.type == STRING: - attr_dict[public_alias] = { + attr_dict[name] = { "value": a.value.val_str, - "type": "str", } elif a.key.type == DOUBLE: - attr_dict[public_alias] = { + attr_dict[name] = { "value": a.value.val_double, - "type": "double", } elif a.key.type == BOOLEAN: - attr_dict[public_alias] = { + attr_dict[name] = { "value": a.value.val_bool, - "type": "bool", } elif a.key.type == INT: if r and r.search_type == "boolean": - attr_dict[public_alias] = { + attr_dict[name] = { "value": a.value.val_int == 1, - "type": "bool", } else: - attr_dict[public_alias] = { + attr_dict[name] = { "value": a.value.val_int, - "type": "int", } - if public_alias == "project.id": + if name == "project.id": # Enrich with project slug, alias "project" attr_dict["project"] = { "value": resolver.params.project_id_map.get(a.value.val_int, "Unknown"), - "type": "str", } item_dicts.append( @@ -1176,8 +1173,6 @@ def get_log_attributes_for_trace( ) if not message_substring: - if limit is not None: - items = items[:limit] # Re-apply in case the endpoint didn't respect it. return {"data": items} # Filter on message substring. @@ -1253,8 +1248,6 @@ def get_metric_attributes_for_trace( ) if not metric_name: - if limit is not None: - items = items[:limit] # Re-apply in case the endpoint didn't respect it. return {"data": items} # Filter on metric name (exact case-insensitive match). diff --git a/src/sentry/snuba/referrer.py b/src/sentry/snuba/referrer.py index 47f680878b46d1..ba670f13561767 100644 --- a/src/sentry/snuba/referrer.py +++ b/src/sentry/snuba/referrer.py @@ -295,6 +295,9 @@ class Referrer(StrEnum): ) API_INSIGHTS_TRANSACTION_EVENTS = "api.insights.transaction-events" API_INSIGHTS_LANDING_TABLE = "api.insights.landing-table" + API_INSIGHTS_LANDING_TABLE_METRICS_ENHANCED_PRIMARY = ( + "api.insights.landing-table.metrics-enhanced.primary" + ) API_INSIGHTS_STATUS_BREAKDOWN = "api.insights.status-breakdown" API_INSIGHTS_DURATIONPERCENTILECHART = "api.insights.durationpercentilechart" @@ -416,6 +419,9 @@ class Referrer(StrEnum): API_ISSUES_RELATED_ISSUES = "api.issues.related_issues" API_METRICS_TOTALS = "api.metrics.totals" API_METRICS_TOTALS_INITIAL_QUERY = "api.metrics.totals.initial_query" + API_METRICS_TOTALS_SECOND_QUERY = "api.metrics.totals.second_query" + API_METRICS_SERIES_SECOND_QUERY = "api.metrics.series.second_query" + API_ORGANIZATION_TRACE_ITEM_DETAILS = "api.organization-trace-item-details" API_ORGANIZATION_EVENT_STATS_FIND_TOPN = "api.organization-event-stats.find-topn" API_ORGANIZATION_EVENT_STATS_METRICS_ENHANCED = "api.organization-event-stats.metrics-enhanced" @@ -446,6 +452,7 @@ class Referrer(StrEnum): API_ORGANIZATION_EVENTS_METRICS_COMPATIBILITY_SUM_METRICS_METRICS_ENHANCED_PRIMARY = ( "api.organization-events-metrics-compatibility.sum_metrics.metrics-enhanced.primary" ) + API_ORGANIZATION_EVENTS_METRICS_ENHANCED = "api.organization-events.metrics-enhanced" API_ORGANIZATION_EVENTS_METRICS_ENHANCED_PRIMARY = ( "api.organization-events.metrics-enhanced.primary" ) diff --git a/src/sentry/snuba/rpc_dataset_common.py b/src/sentry/snuba/rpc_dataset_common.py index e84d7dcbf78525..0e60bc0957c749 100644 --- a/src/sentry/snuba/rpc_dataset_common.py +++ b/src/sentry/snuba/rpc_dataset_common.py @@ -7,6 +7,9 @@ import sentry_sdk from google.protobuf.json_format import MessageToJson +from sentry_protos.snuba.v1.attribute_conditional_aggregation_pb2 import ( + AttributeConditionalAggregation, +) from sentry_protos.snuba.v1.downsampled_storage_pb2 import DownsampledStorageConfig from sentry_protos.snuba.v1.endpoint_time_series_pb2 import ( Expression, @@ -19,6 +22,7 @@ TraceItemTableRequest, TraceItemTableResponse, ) +from sentry_protos.snuba.v1.formula_pb2 import Literal from sentry_protos.snuba.v1.request_common_pb2 import ( PageToken, RequestMeta, @@ -26,7 +30,12 @@ TraceItemFilterWithType, TraceItemType, ) -from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue, Function +from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ( + AttributeAggregation, + AttributeKey, + AttributeValue, + Function, +) from sentry_protos.snuba.v1.trace_item_filter_pb2 import ( AndFilter, ComparisonFilter, @@ -37,16 +46,7 @@ from sentry.api.event_search import SearchFilter, SearchKey, SearchValue from sentry.discover import arithmetic from sentry.exceptions import InvalidSearchQuery -from sentry.search.eap.columns import ( - AnyResolved, - ColumnDefinitions, - ResolvedAggregate, - ResolvedAttribute, - ResolvedConditionalAggregate, - ResolvedEquation, - ResolvedFormula, - ResolvedLiteral, -) +from sentry.search.eap.columns import ColumnDefinitions, ResolvedAttribute, ResolvedColumn from sentry.search.eap.constants import DOUBLE, MAX_ROLLUP_POINTS, VALID_GRANULARITIES from sentry.search.eap.resolver import SearchResolver from sentry.search.eap.rpc_utils import and_trace_item_filters @@ -97,7 +97,7 @@ class TableRequest: """Container for rpc requests""" rpc_request: TraceItemTableRequest - columns: list[AnyResolved] + columns: list[ResolvedColumn] def check_timeseries_has_data(timeseries: SnubaData, y_axes: list[str]): @@ -140,39 +140,48 @@ def get_resolver( @classmethod def categorize_column( cls, - column: AnyResolved, + column: ResolvedColumn, ) -> Column: - # Can't do bare literals, so they're actually formulas with +0 - if isinstance(column, (ResolvedFormula, ResolvedEquation, ResolvedLiteral)): - return Column(formula=column.proto_definition, label=column.public_alias) - elif isinstance(column, ResolvedAggregate): - return Column(aggregation=column.proto_definition, label=column.public_alias) - elif isinstance(column, ResolvedConditionalAggregate): - return Column( - conditional_aggregation=column.proto_definition, label=column.public_alias - ) - else: - return Column(key=column.proto_definition, label=column.public_alias) + proto_definition = column.proto_definition + + if isinstance(proto_definition, AttributeKey): + return Column(key=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, AttributeAggregation): + return Column(aggregation=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, AttributeConditionalAggregation): + return Column(conditional_aggregation=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, Column.BinaryFormula): + return Column(formula=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, Literal): + return Column(literal=proto_definition, label=column.public_alias) + + raise TypeError(f"Unsupported proto definition type: {type(proto_definition)}") @classmethod def categorize_aggregate( cls, - column: AnyResolved, + column: ResolvedColumn, ) -> Expression: - if isinstance(column, (ResolvedFormula, ResolvedEquation)): + proto_definition = column.proto_definition + + if isinstance(proto_definition, AttributeAggregation): + return Expression(aggregation=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, AttributeConditionalAggregation): + return Expression(conditional_aggregation=proto_definition, label=column.public_alias) + + if isinstance(proto_definition, Column.BinaryFormula): # TODO: Remove when https://github.com/getsentry/eap-planning/issues/206 is merged, since we can use formulas in both APIs at that point return Expression( - formula=transform_binary_formula_to_expression(column.proto_definition), + formula=transform_binary_formula_to_expression(proto_definition), label=column.public_alias, ) - elif isinstance(column, ResolvedAggregate): - return Expression(aggregation=column.proto_definition, label=column.public_alias) - elif isinstance(column, ResolvedConditionalAggregate): - return Expression( - conditional_aggregation=column.proto_definition, label=column.public_alias - ) - else: - raise Exception(f"Unknown column type {type(column)}") + + raise TypeError(f"Unsupported proto definition type: {type(proto_definition)}") @classmethod def get_cross_trace_queries(cls, query: TableQuery) -> list[TraceItemFilterWithType]: @@ -251,7 +260,7 @@ def get_table_rpc_request(cls, query: TableQuery) -> TableRequest: # incomplete traces. meta.downsampled_storage_config.mode = DownsampledStorageConfig.MODE_HIGHEST_ACCURACY - all_columns: list[AnyResolved] = [] + all_columns: list[ResolvedColumn] = [] equations, equation_contexts = resolver.resolve_equations( query.equations if query.equations else [] ) @@ -594,7 +603,7 @@ def get_timeseries_query( extra_conditions: TraceItemFilter | None = None, ) -> tuple[ TimeSeriesRequest, - list[AnyResolved], + list[ResolvedColumn], list[ResolvedAttribute], ]: selected_equations, selected_axes = arithmetic.categorize_columns(y_axes) diff --git a/src/sentry/tasks/auto_ongoing_issues.py b/src/sentry/tasks/auto_ongoing_issues.py index c79a7868bd6470..ff56e0fb5af5cb 100644 --- a/src/sentry/tasks/auto_ongoing_issues.py +++ b/src/sentry/tasks/auto_ongoing_issues.py @@ -2,11 +2,11 @@ from datetime import datetime, timedelta, timezone import sentry_sdk -from django.db.models import Max +from django.db.models import Max, OuterRef, Subquery from sentry.issues.ongoing import TRANSITION_AFTER_DAYS, bulk_transition_group_to_ongoing from sentry.models.group import Group, GroupStatus -from sentry.models.grouphistory import GroupHistoryStatus +from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import issues_tasks @@ -163,14 +163,28 @@ def get_total_count(results): nonlocal total_count total_count += len(results) + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + # Use a subquery to get the most recent REGRESSED history date for each group. + # This ensures we only transition groups whose MOST RECENT regressed history + # is older than the threshold, not just any regressed history. + latest_regressed_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.REGRESSED) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] + ) + base_queryset = ( Group.objects.filter( status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.REGRESSED, - grouphistory__status=GroupHistoryStatus.REGRESSED, ) - .annotate(recent_regressed_history=Max("grouphistory__date_added")) - .filter(recent_regressed_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .annotate(recent_regressed_history=Subquery(latest_regressed_subquery)) + .filter( + recent_regressed_history__lte=date_threshold, + recent_regressed_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): @@ -244,14 +258,30 @@ def get_total_count(results): nonlocal total_count total_count += len(results) + from django.db.models import Max, OuterRef, Subquery + + date_threshold = datetime.fromtimestamp(date_added_lte, timezone.utc) + + # Use a subquery to get the most recent ESCALATING history date for each group. + # This ensures we only transition groups whose MOST RECENT escalating history + # is older than the threshold, not just any escalating history. + latest_escalating_subquery = ( + GroupHistory.objects.filter(group_id=OuterRef("id"), status=GroupHistoryStatus.ESCALATING) + .values("group_id") + .annotate(max_date=Max("date_added")) + .values("max_date")[:1] + ) + base_queryset = ( Group.objects.filter( status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.ESCALATING, - grouphistory__status=GroupHistoryStatus.ESCALATING, ) - .annotate(recent_escalating_history=Max("grouphistory__date_added")) - .filter(recent_escalating_history__lte=datetime.fromtimestamp(date_added_lte, timezone.utc)) + .annotate(recent_escalating_history=Subquery(latest_escalating_subquery)) + .filter( + recent_escalating_history__lte=date_threshold, + recent_escalating_history__isnull=False, + ) ) with sentry_sdk.start_span(name="iterate_chunked_group_ids"): diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index d3330a01e082ee..5a8aab091e899a 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1618,7 +1618,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: group = event.group # Default behaviour - if not features.has("projects:triage-signals-v0", group.project): + if not features.has("organizations:triage-signals-v0-org", group.organization): # Only run on issues with no existing scan if group.seer_fixability_score is not None: return diff --git a/src/sentry/tempest/tasks.py b/src/sentry/tempest/tasks.py index 70ae4a18d672c3..35bec1a2493481 100644 --- a/src/sentry/tempest/tasks.py +++ b/src/sentry/tempest/tasks.py @@ -71,8 +71,8 @@ def fetch_latest_item_id(credentials_id: int, **kwargs) -> None: if result["latest_id"] is None: # If there are no crashes in the CRS we want to communicate that back to the # customer so that they are not surprised about no crashes arriving. - credentials.message = "No crashes found" - credentials.message_type = MessageType.ERROR + credentials.message = "Connection successful. No crashes found in the crash report system yet. New crashes will appear here automatically when they occur." + credentials.message_type = MessageType.WARNING credentials.save(update_fields=["message", "message_type"]) return else: diff --git a/src/sentry/tempest/utils.py b/src/sentry/tempest/utils.py index 24f7cc3ff0d168..53d2caedb88133 100644 --- a/src/sentry/tempest/utils.py +++ b/src/sentry/tempest/utils.py @@ -1,12 +1,9 @@ from sentry.models.organization import Organization +from sentry.utils.console_platforms import organization_has_console_platform_access def has_tempest_access(organization: Organization | None) -> bool: - if not organization: return False - enabled_platforms = organization.get_option("sentry:enabled_console_platforms", []) - has_playstation_access = "playstation" in enabled_platforms - - return has_playstation_access + return organization_has_console_platform_access(organization, "playstation") diff --git a/src/sentry/utils/console_platforms.py b/src/sentry/utils/console_platforms.py new file mode 100644 index 00000000000000..96544e116beb42 --- /dev/null +++ b/src/sentry/utils/console_platforms.py @@ -0,0 +1,19 @@ +from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT +from sentry.models.organization import Organization + + +def organization_has_console_platform_access(organization: Organization, platform: str) -> bool: + """ + Check if an organization has access to a specific console platform. + + Args: + organization: The organization to check + platform: The console platform (e.g., 'nintendo-switch', 'playstation', 'xbox') + + Returns: + True if the organization has access to the console platform, False otherwise + """ + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + return platform in enabled_console_platforms diff --git a/src/sentry/utils/snowflake.py b/src/sentry/utils/snowflake.py index 0f1036c528dd96..4148e6f0f6fe25 100644 --- a/src/sentry/utils/snowflake.py +++ b/src/sentry/utils/snowflake.py @@ -8,12 +8,13 @@ from django.conf import settings from django.db import IntegrityError, router, transaction from django.db.models import Model -from redis.client import StrictRedis from rest_framework import status from rest_framework.exceptions import APIException +from sentry_redis_tools.clients import RedisCluster, StrictRedis from sentry.db.postgres.transactions import enforce_constraints from sentry.types.region import RegionContextError, get_local_region +from sentry.utils import redis if TYPE_CHECKING: from sentry.db.models.base import Model as BaseModel @@ -139,14 +140,16 @@ def generate_snowflake_id(redis_key: str) -> int: return snowflake_id -def get_redis_cluster(redis_key: str) -> StrictRedis[str]: - from sentry.utils import redis +def get_redis_cluster() -> RedisCluster[str] | StrictRedis[str]: + return redis.redis_clusters.get(settings.SENTRY_SNOWFLAKE_REDIS_CLUSTER) - return redis.clusters.get("default").get_local_client_for_key(redis_key) + +def get_timestamp_redis_key(redis_key: str, timestamp: int) -> str: + return f"snowflakeid:{redis_key}:{str(timestamp)}" def get_sequence_value_from_redis(redis_key: str, starting_timestamp: int) -> tuple[int, int]: - cluster = get_redis_cluster(redis_key) + cluster = get_redis_cluster() # this is the amount we want to lookback for previous timestamps # the below is more of a safety net if starting_timestamp is ever @@ -156,14 +159,16 @@ def get_sequence_value_from_redis(redis_key: str, starting_timestamp: int) -> tu for i in range(time_range): timestamp = starting_timestamp - i + timestamp_redis_key = get_timestamp_redis_key(redis_key, timestamp) + # We are decreasing the value by 1 each time since the incr operation in redis # initializes the counter at 1. For our region sequences, we want the value to # be from 0-15 and not 1-16 - sequence_value = cluster.incr(str(timestamp)) + sequence_value = cluster.incr(timestamp_redis_key) sequence_value -= 1 if sequence_value == 0: - cluster.expire(str(timestamp), int(_TTL.total_seconds())) + cluster.expire(timestamp_redis_key, int(_TTL.total_seconds())) if sequence_value < MAX_AVAILABLE_REGION_SEQUENCES: return timestamp, sequence_value diff --git a/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py b/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py index 5f6ce8f624480d..2e30348fc5a8f3 100644 --- a/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py +++ b/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py @@ -38,7 +38,9 @@ def get(self, request: Request, organization: Organization, detector_id: str) -> """ Return anomaly detection threshold data (yhat_lower, yhat_upper) for a detector. """ - if not features.has("organizations:anomaly-detection-threshold-data", organization): + if not features.has( + "organizations:anomaly-detection-threshold-data", organization, actor=request.user + ): raise ResourceDoesNotExist try: diff --git a/src/sentry/workflow_engine/endpoints/organization_incident_groupopenperiod_index.py b/src/sentry/workflow_engine/endpoints/organization_incident_groupopenperiod_index.py index 3db21cac32de7c..ae748de96dfe95 100644 --- a/src/sentry/workflow_engine/endpoints/organization_incident_groupopenperiod_index.py +++ b/src/sentry/workflow_engine/endpoints/organization_incident_groupopenperiod_index.py @@ -15,6 +15,8 @@ RESPONSE_UNAUTHORIZED, ) from sentry.apidocs.parameters import GlobalParams +from sentry.incidents.endpoints.serializers.utils import get_object_id_from_fake_id +from sentry.models.groupopenperiod import GroupOpenPeriod from sentry.workflow_engine.endpoints.serializers.incident_groupopenperiod_serializer import ( IncidentGroupOpenPeriodSerializer, ) @@ -49,6 +51,9 @@ def get(self, request, organization): """ Returns an incident and group open period relationship. Can optionally filter by incident_id, incident_identifier, group_id, or open_period_id. + If incident_identifier is provided but no match is found, falls back to calculating + open_period_id by subtracting 10^9 from the incident_identifier and looking up the + GroupOpenPeriod directly. """ validator = IncidentGroupOpenPeriodValidator(data=request.query_params) validator.is_valid(raise_exception=True) @@ -74,7 +79,37 @@ def get(self, request, organization): queryset = queryset.filter(group_open_period_id=open_period_id) incident_groupopenperiod = queryset.first() - if not incident_groupopenperiod: - raise ResourceDoesNotExist - return Response(serialize(incident_groupopenperiod, request.user)) + if incident_groupopenperiod: + return Response(serialize(incident_groupopenperiod, request.user)) + + # Fallback: if incident_identifier or incident_id was provided but no IGOP found, + # try looking up GroupOpenPeriod directly using calculated open_period_id + fake_id = incident_identifier or incident_id + if fake_id: + calculated_open_period_id = get_object_id_from_fake_id(int(fake_id)) + gop_queryset = GroupOpenPeriod.objects.filter( + id=calculated_open_period_id, + project__organization=organization, + ) + + if group_id: + gop_queryset = gop_queryset.filter(group_id=group_id) + + if open_period_id: + gop_queryset = gop_queryset.filter(id=open_period_id) + + group_open_period = gop_queryset.first() + + if group_open_period: + # Serialize the GroupOpenPeriod as if it were an IncidentGroupOpenPeriod + return Response( + { + "incidentId": str(fake_id), + "incidentIdentifier": str(fake_id), + "groupId": str(group_open_period.group_id), + "openPeriodId": str(group_open_period.id), + } + ) + + raise ResourceDoesNotExist diff --git a/src/sentry/workflow_engine/endpoints/serializers/incident_groupopenperiod_serializer.py b/src/sentry/workflow_engine/endpoints/serializers/incident_groupopenperiod_serializer.py index 99709703f5bdd3..3e8982de911bf3 100644 --- a/src/sentry/workflow_engine/endpoints/serializers/incident_groupopenperiod_serializer.py +++ b/src/sentry/workflow_engine/endpoints/serializers/incident_groupopenperiod_serializer.py @@ -7,7 +7,7 @@ class IncidentGroupOpenPeriodSerializerResponse(TypedDict): incidentId: str | None - incidentIdentifier: int | None + incidentIdentifier: str | None groupId: str openPeriodId: str @@ -19,7 +19,7 @@ def serialize( ) -> IncidentGroupOpenPeriodSerializerResponse: return { "incidentId": str(obj.incident_id) if obj.incident_id else None, - "incidentIdentifier": obj.incident_identifier, + "incidentIdentifier": str(obj.incident_identifier) if obj.incident_identifier else None, "groupId": str(obj.group_open_period.group_id), "openPeriodId": str(obj.group_open_period.id), } diff --git a/src/sentry/workflow_engine/migrations/0104_action_data_fallthrough_type.py b/src/sentry/workflow_engine/migrations/0104_action_data_fallthrough_type.py index e3bdf5a3406ce0..1ae640319f2964 100644 --- a/src/sentry/workflow_engine/migrations/0104_action_data_fallthrough_type.py +++ b/src/sentry/workflow_engine/migrations/0104_action_data_fallthrough_type.py @@ -1,5 +1,7 @@ # Generated by Django 5.2.8 on 2025-11-24 19:57 +import logging + from django.db import migrations from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.migrations.state import StateApps @@ -7,16 +9,28 @@ from sentry.new_migrations.migrations import CheckedMigration from sentry.utils.query import RangeQuerySetWrapper +logger = logging.getLogger(__name__) + def migrate_fallthrough_type(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None: Action = apps.get_model("workflow_engine", "Action") - for action in RangeQuerySetWrapper(Action.objects.all()): + count = 0 + for action in RangeQuerySetWrapper(Action.objects.filter(type="email")): if "fallthroughType" in action.data: new_data = action.data.copy() del new_data["fallthroughType"] new_data["fallthrough_type"] = action.data["fallthroughType"] action.data = new_data action.save() + count += 1 + if count % 1000 == 0: + logger.info( + "Progress update", + extra={ + "count": count, + "current_action_id": action.id, + }, + ) class Migration(CheckedMigration): diff --git a/src/sentry/workflow_engine/typings/notification_action.py b/src/sentry/workflow_engine/typings/notification_action.py index befe29bd72f20c..eb48b24eeb1957 100644 --- a/src/sentry/workflow_engine/typings/notification_action.py +++ b/src/sentry/workflow_engine/typings/notification_action.py @@ -6,6 +6,8 @@ from enum import Enum, IntEnum, StrEnum from typing import Any, ClassVar, NotRequired, TypedDict +from sentry.utils import json + OPSGENIE_DEFAULT_PRIORITY = "P3" PAGERDUTY_DEFAULT_SEVERITY = "default" @@ -643,6 +645,9 @@ def get_sanitized_data(self) -> dict[str, Any]: data = SentryAppDataBlob() if settings := self.action.get("settings"): for setting in settings: + # stringify setting value if it's a list + if isinstance(setting.get("value"), list): + setting["value"] = json.dumps(setting["value"]) data.settings.append(SentryAppFormConfigDataBlob(**setting)) return dataclasses.asdict(data) diff --git a/static/app/actionCreators/modal.tsx b/static/app/actionCreators/modal.tsx index 93701beebf196f..1a3dcca97af7f0 100644 --- a/static/app/actionCreators/modal.tsx +++ b/static/app/actionCreators/modal.tsx @@ -12,7 +12,6 @@ import type {ReprocessEventModalOptions} from 'sentry/components/modals/reproces import type {TokenRegenerationConfirmationModalProps} from 'sentry/components/modals/tokenRegenerationConfirmationModal'; import type {AddToDashboardModalProps} from 'sentry/components/modals/widgetBuilder/addToDashboardModal'; import type {LinkToDashboardModalProps} from 'sentry/components/modals/widgetBuilder/linkToDashboardModal'; -import type {OverwriteWidgetModalProps} from 'sentry/components/modals/widgetBuilder/overwriteWidgetModal'; import type {WidgetViewerModalOptions} from 'sentry/components/modals/widgetViewerModal'; import type {ConsoleModalProps} from 'sentry/components/onboarding/consoleModal'; import type {Category} from 'sentry/components/platformPicker'; @@ -263,19 +262,6 @@ export async function openInviteMissingMembersModal({ }); } -export async function openWidgetBuilderOverwriteModal( - options: OverwriteWidgetModalProps -) { - const {default: Modal, modalCss} = await import( - 'sentry/components/modals/widgetBuilder/overwriteWidgetModal' - ); - - openModal(deps => , { - closeEvents: 'escape-key', - modalCss, - }); -} - export async function openAddToDashboardModal(options: AddToDashboardModalProps) { const {default: Modal, modalCss} = await import( 'sentry/components/modals/widgetBuilder/addToDashboardModal' @@ -328,7 +314,7 @@ export async function demoSignupModal(options: ModalOptions = {}) { openModal(deps => , {modalCss}); } -export type DemoEndModalOptions = { +type DemoEndModalOptions = { tour: string; }; @@ -389,7 +375,7 @@ export async function openCreateReleaseIntegration( openModal(deps => ); } -export type NavigateToExternalLinkModalOptions = { +type NavigateToExternalLinkModalOptions = { linkText: string; }; @@ -488,7 +474,7 @@ export async function openPrivateGamingSdkAccessModal( openModal(deps => ); } -export type InsightInfoModalOptions = { +type InsightInfoModalOptions = { children: React.ReactNode; title: string; }; diff --git a/static/app/bootstrap/initializeSdk.tsx b/static/app/bootstrap/initializeSdk.tsx index 9d3056e2577fc4..1c5013820bbfd9 100644 --- a/static/app/bootstrap/initializeSdk.tsx +++ b/static/app/bootstrap/initializeSdk.tsx @@ -119,7 +119,8 @@ export function initializeSdk(config: Config) { allowUrls: SPA_DSN ? SPA_MODE_ALLOW_URLS : sentryConfig?.allowUrls, integrations: getSentryIntegrations(), tracesSampleRate, - profilesSampleRate: shouldOverrideBrowserProfiling ? 1 : 0.1, + profileSessionSampleRate: shouldOverrideBrowserProfiling ? 1 : 0.1, + profileLifecycle: 'trace', tracePropagationTargets: ['localhost', /^\//, ...extraTracePropagationTargets], tracesSampler: context => { const op = context.attributes?.[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP] || ''; diff --git a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx index 2af075add070ff..1c42befadf568c 100644 --- a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx +++ b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx @@ -103,7 +103,10 @@ export default function ReplayPreviewPlayer({ )} { + const summary = feedbackItem.metadata.summary; + const message = + feedbackItem.metadata.message ?? feedbackItem.metadata.value ?? t('No message'); + const culprit = eventData?.culprit?.trim(); + const viewNames = eventData?.contexts?.app?.view_names?.filter(Boolean); + + const sourceLines = []; + if (culprit) { + sourceLines.push(`- ${culprit}`); + } + if (viewNames?.length) { + sourceLines.push(t('- View names: %s', viewNames.join(', '))); + } + + const markdown = [ + '# User Feedback', + '', + ...(summary ? [`**Summary:** ${summary}`, ''] : []), + '## Feedback Message', + message, + ...(sourceLines.length + ? [ + '', + '## Source (_where user was when feedback was sent_)', + sourceLines.join('\n'), + ] + : []), + ].join('\n'); + + trackAnalytics('feedback.feedback-item-copy-as-markdown', { + organization, + }); + + copy(markdown, { + successMessage: t('Copied feedback'), + errorMessage: t('Failed to copy feedback'), + }); + }, [copy, eventData, feedbackItem, organization]); if (!eventData) { return null; } @@ -42,14 +86,35 @@ export default function FeedbackActions({ /> - {size === 'large' ? : null} - {size === 'medium' ? : null} - {size === 'small' ? : null} + {size === 'large' ? ( + + ) : null} + {size === 'medium' ? ( + + ) : null} + {size === 'small' ? ( + + ) : null} ); } -function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) { +function LargeWidth({ + feedbackItem, + onCopyToClipboard, +}: { + feedbackItem: FeedbackIssue; + onCopyToClipboard: () => void; +}) { const { enableDelete, onDelete, @@ -82,6 +147,15 @@ function LargeWidth({feedbackItem}: {feedbackItem: FeedbackIssue}) { {hasSeen ? t('Mark Unread') : t('Mark Read')} + + - - - - - ); -} - -export default OverwriteWidgetModal; - -export const modalCss = css` - width: 100%; - max-width: 700px; - margin: 70px auto; -`; - -const CardWrapper = styled('div')` - padding: ${space(3)} 0; -`; diff --git a/static/app/components/organizations/pageFilters/container.tsx b/static/app/components/organizations/pageFilters/container.tsx index 25bfd5efc324b5..afa8fe4a010812 100644 --- a/static/app/components/organizations/pageFilters/container.tsx +++ b/static/app/components/organizations/pageFilters/container.tsx @@ -14,6 +14,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DEFAULT_STATS_PERIOD} from 'sentry/constants'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {useLocation} from 'sentry/utils/useLocation'; +import {useDefaultMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; @@ -87,6 +88,9 @@ function PageFiltersContainer({ ? [] : specifiedProjects.filter(project => !project.isMember); + const defaultMaxPickableDays = useDefaultMaxPickableDays(); + maxPickableDays = maxPickableDays ?? defaultMaxPickableDays; + const doInitialization = () => { initializeUrlState({ organization, diff --git a/static/app/components/platformList.tsx b/static/app/components/platformList.tsx index 8e8de327bbe080..84414b7a1e9b5e 100644 --- a/static/app/components/platformList.tsx +++ b/static/app/components/platformList.tsx @@ -1,4 +1,3 @@ -import type {Theme} from '@emotion/react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {PlatformIcon} from 'platformicons'; @@ -62,41 +61,19 @@ function getOverlapWidth(size: number) { return Math.round(size / 4); } -const commonStyles = ({theme}: {theme: Theme}) => css` - cursor: default; - border-radius: ${theme.borderRadius}; - box-shadow: 0 0 0 1px ${theme.background}; - :hover { - z-index: 1; - } -`; - const PlatformIcons = styled('div')` display: flex; `; -const InnerWrapper = styled('div')` - display: flex; - position: relative; -`; - const StyledPlatformIcon = styled(PlatformIcon)` - ${p => commonStyles(p)}; -`; - -const Counter = styled('div')` - ${p => commonStyles(p)}; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - font-weight: ${p => p.theme.fontWeight.bold}; - font-size: ${p => p.theme.fontSize.xs}; - background-color: ${p => p.theme.gray200}; - color: ${p => p.theme.subText}; - padding: 0 1px; - position: absolute; - right: -1px; + cursor: default; + border-radius: ${p => p.theme.borderRadius}; + box-shadow: 0 0 0 1px ${p => p.theme.background}; + :hover { + z-index: 1; + } + height: ${p => p.size}px; + min-width: ${p => p.size}px; `; const Wrapper = styled('div')` @@ -111,13 +88,4 @@ const Wrapper = styled('div')` } `} } - - ${InnerWrapper} { - padding-right: ${p => p.size / 2 + 1}px; - } - - ${Counter} { - height: ${p => p.size}px; - min-width: ${p => p.size}px; - } `; diff --git a/static/app/components/replays/header/configureReplayCard.tsx b/static/app/components/replays/header/configureReplayCard.tsx index 3e176320795f49..d40f907ba08e3e 100644 --- a/static/app/components/replays/header/configureReplayCard.tsx +++ b/static/app/components/replays/header/configureReplayCard.tsx @@ -33,7 +33,7 @@ export default function ConfigureReplayCard({ }} items={isMobile ? getMobileItems(replayRecord) : getWebItems()} trigger={(triggerProps, isOpen) => ( - + {t('Configure Replay')} )} diff --git a/static/app/components/replays/header/replayMetaData.tsx b/static/app/components/replays/header/replayMetaData.tsx index 4415b73ca6deb9..129357194cc28f 100644 --- a/static/app/components/replays/header/replayMetaData.tsx +++ b/static/app/components/replays/header/replayMetaData.tsx @@ -114,9 +114,9 @@ const KeyMetrics = styled('dl')` grid-template-rows: max-content 1fr; grid-template-columns: repeat(4, max-content); grid-auto-flow: column; + height: 42px; gap: 0 ${space(3)}; align-items: center; - align-self: end; color: ${p => p.theme.subText}; margin: 0; @@ -126,12 +126,12 @@ const KeyMetrics = styled('dl')` `; const KeyMetricLabel = styled('dt')` - font-size: ${p => p.theme.fontSize.md}; + font-size: ${p => p.theme.fontSize.sm}; `; const KeyMetricData = styled('dd')` - font-size: ${p => p.theme.fontSize.xl}; - font-weight: ${p => p.theme.fontWeight.normal}; + font-size: ${p => p.theme.fontSize.md}; + font-weight: ${p => p.theme.fontWeight.bold}; display: flex; align-items: center; gap: ${space(1)}; diff --git a/static/app/components/replays/header/replayViewers.tsx b/static/app/components/replays/header/replayViewers.tsx index 7957949cbd8d3e..906ef3c9ca0652 100644 --- a/static/app/components/replays/header/replayViewers.tsx +++ b/static/app/components/replays/header/replayViewers.tsx @@ -29,7 +29,7 @@ export default function ReplayViewers({projectId, replayId}: Props) { }); return isPending || isError ? ( - + ) : ( ); diff --git a/static/app/components/replays/replayBadge.tsx b/static/app/components/replays/replayBadge.tsx index e78c9969ccb81e..79bb82bb89d54f 100644 --- a/static/app/components/replays/replayBadge.tsx +++ b/static/app/components/replays/replayBadge.tsx @@ -124,13 +124,13 @@ export default function ReplayBadge({replay}: Props) { - + {timestampType === 'absolute' ? ( ) : ( )} - + @@ -138,6 +138,11 @@ export default function ReplayBadge({replay}: Props) { ); } +// We need to use relative position to create a new stacking context for tooltip +const RelativeText = styled(Text)` + position: relative; +`; + const Wrapper = styled(Grid)` white-space: nowrap; `; diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index 150f7d04b7bb72..8891ed79b39581 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -12,6 +12,8 @@ import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; import type RequestError from 'sentry/utils/requestError/requestError'; import {ERROR_MAP} from 'sentry/utils/requestError/requestError'; +import useOrganization from 'sentry/utils/useOrganization'; +import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; import type {ReplayListRecord} from 'sentry/views/replays/types'; type SortProps = @@ -49,6 +51,7 @@ export default function ReplayTable({ }: Props) { const gridTemplateColumns = columns.map(col => col.width ?? 'max-content').join(' '); const hasInteractiveColumn = columns.some(col => col.interactive); + const organization = useOrganization(); if (isPending) { return ( @@ -126,7 +129,10 @@ export default function ReplayTable({ {columns.map((column, columnIndex) => ( { - const organization = useOrganization(); + Component: ({linkQuery}) => { return ( - + @@ -511,7 +505,7 @@ export const ReplaySessionColumn: ReplayTableColumn = { interactive: true, sortKey: 'started_at', width: 'minmax(150px, 1fr)', - Component: ({replay, query}) => { + Component: ({replay, linkQuery, className}) => { const routes = useRoutes(); const referrer = getRouteStringFromRoutes(routes); @@ -537,13 +531,7 @@ export const ReplaySessionColumn: ReplayTableColumn = { }); return ( - + ); diff --git a/static/app/components/themeAndStyleProvider.tsx b/static/app/components/themeAndStyleProvider.tsx index 174e8e6d05e525..6c58ecc506d29c 100644 --- a/static/app/components/themeAndStyleProvider.tsx +++ b/static/app/components/themeAndStyleProvider.tsx @@ -1,4 +1,4 @@ -import {Fragment, lazy} from 'react'; +import {Fragment, lazy, useMemo} from 'react'; import {createPortal} from 'react-dom'; import createCache from '@emotion/cache'; import type {Theme} from '@emotion/react'; @@ -8,7 +8,12 @@ import {NODE_ENV} from 'sentry/constants'; import ConfigStore from 'sentry/stores/configStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import GlobalStyles from 'sentry/styles/global'; -import {useThemeSwitcher} from 'sentry/utils/theme/useThemeSwitcher'; +import {removeBodyTheme} from 'sentry/utils/removeBodyTheme'; +import { + DO_NOT_USE_darkChonkTheme, + DO_NOT_USE_lightChonkTheme, +} from 'sentry/utils/theme/theme.chonk'; +import {useHotkeys} from 'sentry/utils/useHotkeys'; const SentryComponentInspector = NODE_ENV === 'development' @@ -39,11 +44,31 @@ cache.compat = true; */ export function ThemeAndStyleProvider({children}: Props) { const config = useLegacyStore(ConfigStore); - const theme = useThemeSwitcher(); + + // Hotkey definition for toggling the current theme + const themeToggleHotkey = useMemo( + () => [ + { + match: ['command+shift+1', 'ctrl+shift+1'], + includeInputs: true, + callback: () => { + removeBodyTheme(); + ConfigStore.set('theme', config.theme === 'dark' ? 'light' : 'dark'); + }, + }, + ], + [config.theme] + ); + + useHotkeys(themeToggleHotkey); + + const theme = (config.theme === 'dark' + ? DO_NOT_USE_darkChonkTheme + : DO_NOT_USE_lightChonkTheme) as unknown as Theme; return ( - - + + {children} {createPortal( diff --git a/static/app/components/timeRangeSelector/index.tsx b/static/app/components/timeRangeSelector/index.tsx index 66e5027a5f6b86..172132fdbc6308 100644 --- a/static/app/components/timeRangeSelector/index.tsx +++ b/static/app/components/timeRangeSelector/index.tsx @@ -24,6 +24,7 @@ import { } from 'sentry/utils/dates'; import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours'; import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes'; +import {useDefaultMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; import useOrganization from 'sentry/utils/useOrganization'; import useRouter from 'sentry/utils/useRouter'; @@ -153,7 +154,7 @@ export function TimeRangeSelector({ showRelative = true, defaultAbsolute, defaultPeriod = DEFAULT_STATS_PERIOD, - maxPickableDays = 90, + maxPickableDays, maxDateRange, disallowArbitraryRelativeRanges = false, trigger, @@ -167,6 +168,9 @@ export function TimeRangeSelector({ const router = useRouter(); const organization = useOrganization({allowNull: true}); + const defaultMaxPickableDays = useDefaultMaxPickableDays(); + maxPickableDays = maxPickableDays ?? defaultMaxPickableDays; + const [search, setSearch] = useState(''); const [hasChanges, setHasChanges] = useState(false); const [hasDateRangeErrors, setHasDateRangeErrors] = useState(false); diff --git a/static/app/data/platformCategories.tsx b/static/app/data/platformCategories.tsx index 473b5eb7830e36..99bec1259a5b12 100644 --- a/static/app/data/platformCategories.tsx +++ b/static/app/data/platformCategories.tsx @@ -786,6 +786,14 @@ export const agentMonitoringPlatforms: ReadonlySet = new Set([ ]); export const mcpMonitoringPlatforms: ReadonlySet = new Set([ + 'javascript-astro', + 'javascript-nextjs', + 'javascript-nuxt', + 'javascript-react-router', + 'javascript-remix', + 'javascript-solidstart', + 'javascript-sveltekit', + 'javascript-tanstackstart-react', ...platformKeys.filter(id => id.startsWith('node')), ...platformKeys.filter(id => id.startsWith('python')), ]); diff --git a/static/app/gettingStartedDocs/javascript-astro/index.tsx b/static/app/gettingStartedDocs/javascript-astro/index.tsx index 417089305e2e63..a3044121d43c41 100644 --- a/static/app/gettingStartedDocs/javascript-astro/index.tsx +++ b/static/app/gettingStartedDocs/javascript-astro/index.tsx @@ -7,6 +7,7 @@ import {profilingFullStack} from 'sentry/gettingStartedDocs/javascript/profiling import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {replay} from './replay'; @@ -32,6 +33,7 @@ const docs: Docs = { packageName: '@sentry/astro', }), agentMonitoringOnboarding: agentMonitoring, + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-astro/mcp.tsx b/static/app/gettingStartedDocs/javascript-astro/mcp.tsx new file mode 100644 index 00000000000000..626e59343c5280 --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-astro/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/astro', +}); diff --git a/static/app/gettingStartedDocs/javascript-nextjs/index.tsx b/static/app/gettingStartedDocs/javascript-nextjs/index.tsx index 1669638d6db523..228f8b58582e18 100644 --- a/static/app/gettingStartedDocs/javascript-nextjs/index.tsx +++ b/static/app/gettingStartedDocs/javascript-nextjs/index.tsx @@ -8,6 +8,7 @@ import {tct} from 'sentry/locale'; import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {performance} from './performance'; import {replay} from './replay'; @@ -87,6 +88,7 @@ const docs: Docs = { packageName: '@sentry/nextjs', }), agentMonitoringOnboarding: agentMonitoring, + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-nextjs/mcp.tsx b/static/app/gettingStartedDocs/javascript-nextjs/mcp.tsx new file mode 100644 index 00000000000000..4ea5283adb8b81 --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-nextjs/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/nextjs', +}); diff --git a/static/app/gettingStartedDocs/javascript-nuxt/index.tsx b/static/app/gettingStartedDocs/javascript-nuxt/index.tsx index 5292724f122693..890e049eadad82 100644 --- a/static/app/gettingStartedDocs/javascript-nuxt/index.tsx +++ b/static/app/gettingStartedDocs/javascript-nuxt/index.tsx @@ -7,6 +7,7 @@ import {profiling} from 'sentry/gettingStartedDocs/javascript/profiling'; import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {replay} from './replay'; import {installSnippetBlock} from './utils'; @@ -31,6 +32,7 @@ const docs: Docs = { packageName: '@sentry/nuxt', }), agentMonitoringOnboarding: agentMonitoring, + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-nuxt/mcp.tsx b/static/app/gettingStartedDocs/javascript-nuxt/mcp.tsx new file mode 100644 index 00000000000000..045be15ea348b1 --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-nuxt/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/nuxt', +}); diff --git a/static/app/gettingStartedDocs/javascript-react-router/index.tsx b/static/app/gettingStartedDocs/javascript-react-router/index.tsx index 4874cabd353eb4..731f4ed878a102 100644 --- a/static/app/gettingStartedDocs/javascript-react-router/index.tsx +++ b/static/app/gettingStartedDocs/javascript-react-router/index.tsx @@ -6,6 +6,7 @@ import {profilingFullStack} from 'sentry/gettingStartedDocs/javascript/profiling import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {performance} from './performance'; import {replay} from './replay'; @@ -32,6 +33,7 @@ const docs: Docs = { docsPlatform: 'react-router', packageName: '@sentry/react-router', }), + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-react-router/mcp.tsx b/static/app/gettingStartedDocs/javascript-react-router/mcp.tsx new file mode 100644 index 00000000000000..9f02737b82a911 --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-react-router/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/react-router', +}); diff --git a/static/app/gettingStartedDocs/javascript-remix/index.tsx b/static/app/gettingStartedDocs/javascript-remix/index.tsx index 0b52b601f46789..f3d2ea8c929b6a 100644 --- a/static/app/gettingStartedDocs/javascript-remix/index.tsx +++ b/static/app/gettingStartedDocs/javascript-remix/index.tsx @@ -7,6 +7,7 @@ import {profilingFullStack} from 'sentry/gettingStartedDocs/javascript/profiling import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {replay} from './replay'; @@ -32,6 +33,7 @@ const docs: Docs = { docsPlatform: 'remix', packageName: '@sentry/remix', }), + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-remix/mcp.tsx b/static/app/gettingStartedDocs/javascript-remix/mcp.tsx new file mode 100644 index 00000000000000..1677c313cbf43b --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-remix/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/remix', +}); diff --git a/static/app/gettingStartedDocs/javascript-solidstart/index.tsx b/static/app/gettingStartedDocs/javascript-solidstart/index.tsx index 1dfdd766ea4616..c67539a889375f 100644 --- a/static/app/gettingStartedDocs/javascript-solidstart/index.tsx +++ b/static/app/gettingStartedDocs/javascript-solidstart/index.tsx @@ -7,6 +7,7 @@ import {profiling} from 'sentry/gettingStartedDocs/javascript/profiling'; import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {replay} from './replay'; import {installSnippetBlock} from './utils'; @@ -31,6 +32,7 @@ const docs: Docs = { docsPlatform: 'solidstart', packageName: '@sentry/solidstart', }), + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-solidstart/mcp.tsx b/static/app/gettingStartedDocs/javascript-solidstart/mcp.tsx new file mode 100644 index 00000000000000..a8a6bde7a9363b --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-solidstart/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/solidstart', +}); diff --git a/static/app/gettingStartedDocs/javascript-sveltekit/index.tsx b/static/app/gettingStartedDocs/javascript-sveltekit/index.tsx index 173a3871847824..25f323597d0b8d 100644 --- a/static/app/gettingStartedDocs/javascript-sveltekit/index.tsx +++ b/static/app/gettingStartedDocs/javascript-sveltekit/index.tsx @@ -7,6 +7,7 @@ import {profilingFullStack} from 'sentry/gettingStartedDocs/javascript/profiling import {agentMonitoring} from './agentMonitoring'; import {crashReport} from './crashReport'; import {feedback} from './feedback'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; import {replay} from './replay'; @@ -32,6 +33,7 @@ const docs: Docs = { docsPlatform: 'sveltekit', packageName: '@sentry/sveltekit', }), + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-sveltekit/mcp.tsx b/static/app/gettingStartedDocs/javascript-sveltekit/mcp.tsx new file mode 100644 index 00000000000000..c5b490e2d058ca --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-sveltekit/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/sveltekit', +}); diff --git a/static/app/gettingStartedDocs/javascript-tanstackstart-react/index.tsx b/static/app/gettingStartedDocs/javascript-tanstackstart-react/index.tsx index 12f920b6ca6d3c..72d70d5035ca06 100644 --- a/static/app/gettingStartedDocs/javascript-tanstackstart-react/index.tsx +++ b/static/app/gettingStartedDocs/javascript-tanstackstart-react/index.tsx @@ -4,6 +4,7 @@ import {metricsFullStack} from 'sentry/gettingStartedDocs/javascript/metrics'; import {profilingFullStack} from 'sentry/gettingStartedDocs/javascript/profiling'; import {agentMonitoring} from './agentMonitoring'; +import {mcp} from './mcp'; import {onboarding} from './onboarding'; const docs: Docs = { @@ -24,6 +25,7 @@ const docs: Docs = { docsPlatform: 'tanstackstart-react', packageName: '@sentry/tanstackstart-react', }), + mcpOnboarding: mcp, }; export default docs; diff --git a/static/app/gettingStartedDocs/javascript-tanstackstart-react/mcp.tsx b/static/app/gettingStartedDocs/javascript-tanstackstart-react/mcp.tsx new file mode 100644 index 00000000000000..777f870122dd5c --- /dev/null +++ b/static/app/gettingStartedDocs/javascript-tanstackstart-react/mcp.tsx @@ -0,0 +1,5 @@ +import {getNodeMcpOnboarding} from 'sentry/gettingStartedDocs/node/utils'; + +export const mcp = getNodeMcpOnboarding({ + packageName: '@sentry/tanstackstart-react', +}); diff --git a/static/app/gettingStartedDocs/node/utils.tsx b/static/app/gettingStartedDocs/node/utils.tsx index b9f6306cfbdbb0..0f84807f110114 100644 --- a/static/app/gettingStartedDocs/node/utils.tsx +++ b/static/app/gettingStartedDocs/node/utils.tsx @@ -799,7 +799,7 @@ export const getNodeMcpOnboarding = ({ const mcpSdkStep: ContentBlock[] = [ { type: 'text', - text: tct('Initialize the Sentry SDK with [code:Sentry.init()] call.', { + text: tct('Initialize the Sentry SDK by calling [code:Sentry.init()]:', { code: , }), }, diff --git a/static/app/types/hooks.tsx b/static/app/types/hooks.tsx index ca951e6d25d9dc..7f1a4d2501cc78 100644 --- a/static/app/types/hooks.tsx +++ b/static/app/types/hooks.tsx @@ -8,7 +8,10 @@ import type {ProductSelectionProps} from 'sentry/components/onboarding/productSe import type DateRange from 'sentry/components/timeRangeSelector/dateRange'; import type SelectorItems from 'sentry/components/timeRangeSelector/selectorItems'; import type {SentryRouteObject} from 'sentry/router/types'; -import type {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; +import type { + useDefaultMaxPickableDays, + useMaxPickableDays, +} from 'sentry/utils/useMaxPickableDays'; import type {WidgetType} from 'sentry/views/dashboards/types'; import type {OrganizationStatsProps} from 'sentry/views/organizationStats'; import type {RouteAnalyticsContext} from 'sentry/views/routeAnalyticsContextProvider'; @@ -331,6 +334,7 @@ type ReactHooks = { 'react-hook:use-dashboard-dataset-retention-limit': (props: { dataset: WidgetType; }) => number; + 'react-hook:use-default-max-pickable-days': typeof useDefaultMaxPickableDays; 'react-hook:use-get-max-retention-days': () => number | undefined; 'react-hook:use-max-pickable-days': typeof useMaxPickableDays; 'react-hook:use-metric-detector-limit': () => { diff --git a/static/app/utils/analytics/feedbackAnalyticsEvents.tsx b/static/app/utils/analytics/feedbackAnalyticsEvents.tsx index bc07f8bf7c23c6..ba2ce798ee0888 100644 --- a/static/app/utils/analytics/feedbackAnalyticsEvents.tsx +++ b/static/app/utils/analytics/feedbackAnalyticsEvents.tsx @@ -2,6 +2,7 @@ export type FeedbackEventParameters = { 'feedback.details-integration-issue-clicked': { integration_key: string; }; + 'feedback.feedback-item-copy-as-markdown': Record; 'feedback.feedback-item-not-found': {feedbackId: string}; 'feedback.feedback-item-rendered': Record; 'feedback.index-setup-viewed': Record; @@ -28,6 +29,7 @@ export type FeedbackEventParameters = { type FeedbackEventKey = keyof FeedbackEventParameters; export const feedbackEventMap: Record = { + 'feedback.feedback-item-copy-as-markdown': 'Copied Feedback Item as Markdown', 'feedback.feedback-item-not-found': 'Feedback item not found', 'feedback.feedback-item-rendered': 'Loaded and rendered a feedback item', 'feedback.index-setup-viewed': 'Viewed Feedback Onboarding Setup', diff --git a/static/app/utils/issueTypeConfig/aiDetectedConfig.tsx b/static/app/utils/issueTypeConfig/aiDetectedConfig.tsx index f8ead2154a1b5a..092216820127f7 100644 --- a/static/app/utils/issueTypeConfig/aiDetectedConfig.tsx +++ b/static/app/utils/issueTypeConfig/aiDetectedConfig.tsx @@ -33,14 +33,14 @@ const aiDetectedConfig: IssueCategoryConfigMapping = { replays: {enabled: false}, tagsTab: {enabled: false}, }, - autofix: false, + autofix: true, mergedIssues: {enabled: false}, similarIssues: {enabled: false}, stacktrace: {enabled: false}, spanEvidence: {enabled: true}, evidence: null, usesIssuePlatform: true, - issueSummary: {enabled: false}, + issueSummary: {enabled: true}, }, }; diff --git a/static/app/utils/issueTypeConfig/index.tsx b/static/app/utils/issueTypeConfig/index.tsx index 632c53867d4867..34716b60b33ce4 100644 --- a/static/app/utils/issueTypeConfig/index.tsx +++ b/static/app/utils/issueTypeConfig/index.tsx @@ -112,7 +112,7 @@ const issueTypeConfig: Config = { * errors that may otherwise be difficult to debug. For example, common framework * errors that have no stack trace. */ -export function shouldShowCustomErrorResourceConfig( +function shouldShowCustomErrorResourceConfig( params: GetConfigForIssueTypeParams, project: Project ): boolean { diff --git a/static/app/utils/theme/ChonkOptInBanner.tsx b/static/app/utils/theme/ChonkOptInBanner.tsx deleted file mode 100644 index 4a69c220eeb487..00000000000000 --- a/static/app/utils/theme/ChonkOptInBanner.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import {useId} from 'react'; -import type {Theme} from '@emotion/react'; -import {ThemeProvider} from '@emotion/react'; -import styled from '@emotion/styled'; - -import {Button} from 'sentry/components/core/button'; -import Panel from 'sentry/components/panels/panel'; -import {IconClose} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import ConfigStore from 'sentry/stores/configStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; -import {space} from 'sentry/styles/space'; -import {DO_NOT_USE_lightChonkTheme} from 'sentry/utils/theme/theme.chonk'; -import useMutateUserOptions from 'sentry/utils/useMutateUserOptions'; - -import {useChonkPrompt} from './useChonkPrompt'; - -export function ChonkOptInBanner(props: {collapsed: boolean | 'never'}) { - const chonkPrompt = useChonkPrompt(); - const config = useLegacyStore(ConfigStore); - const {mutate: mutateUserOptions} = useMutateUserOptions(); - const id = useId(); - - if (props.collapsed === true || !chonkPrompt.showBannerPrompt) { - return null; - } - - return ( - - {t('Sentry has a new look')} - - {t(`We've updated Sentry with a fresh new look, try it out by opting in below.`)} - - - { - mutateUserOptions({prefersChonkUI: true}); - chonkPrompt.snooze(); - }} - > - {t('Try It Out')} - - - } - aria-label={t('Dismiss')} - onClick={chonkPrompt.snoozeBannerPrompt} - size="xs" - borderless - analyticsEventKey="navigation.banner_dismiss_chonk_ui" - analyticsEventName="Navigation: Chonk UI Banner Dismissed" - /> - - ); -} - -const TranslucentBackgroundPanel = styled(Panel)<{ - isDarkMode: boolean; - position: 'absolute' | 'relative'; -}>` - /* 186px is the same width as what we render in the legacy nav */ - width: ${p => (p.position === 'absolute' ? '186px' : undefined)}; - /* 66px is the same left offset that we need to render due to collapsed nav */ - left: ${p => (p.position === 'absolute' ? '66px' : undefined)}; - position: relative; - background: ${p => p.theme.background}; - border: 1px solid ${p => p.theme.border}; - padding: ${space(1)}; - color: ${p => p.theme.textColor}; - - margin-bottom: ${space(1)}; -`; - -const Title = styled('div')` - font-size: ${p => p.theme.fontSize.sm}; - font-weight: ${p => p.theme.fontWeight.bold}; - margin: 0; - - display: flex; - align-items: center; -`; - -const Description = styled('p')` - font-size: ${p => p.theme.fontSize.sm}; - margin: ${space(0.5)} 0; -`; - -const OptInButton = styled(Button)` - margin: 0 auto; - width: 100%; -`; - -const DismissButton = styled(Button)` - position: absolute; - top: 0; - right: 0; - - color: currentColor; - - &:hover { - color: currentColor; - } -`; diff --git a/static/app/utils/theme/useThemeSwitcher.spec.tsx b/static/app/utils/theme/useThemeSwitcher.spec.tsx deleted file mode 100644 index a567f7dba7c0fa..00000000000000 --- a/static/app/utils/theme/useThemeSwitcher.spec.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import {ConfigFixture} from 'sentry-fixture/config'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {UserFixture} from 'sentry-fixture/user'; - -import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary'; - -import ConfigStore from 'sentry/stores/configStore'; -import OrganizationStore from 'sentry/stores/organizationStore'; -// eslint-disable-next-line no-restricted-imports -- @TODO(jonasbadalic): Remove theme import -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; - -import {DO_NOT_USE_darkChonkTheme, DO_NOT_USE_lightChonkTheme} from './theme.chonk'; -import {useThemeSwitcher} from './useThemeSwitcher'; - -jest.mock('sentry/utils/removeBodyTheme'); - -describe('useChonkTheme', () => { - beforeEach(() => { - localStorage.clear(); - OrganizationStore.reset(); - ConfigStore.loadInitialData( - ConfigFixture({ - theme: 'light', - }) - ); - }); - - describe('disabled states', () => { - it('returns null if no organization is loaded', () => { - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(lightTheme); - }); - - it('returns old theme if the organization does not have chonk-ui feature', () => { - OrganizationStore.onUpdate( - OrganizationFixture({ - features: [], - }) - ); - - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(lightTheme); - }); - - it('returns old theme if the user prefers chonk theme, but the organization does not have chonk-ui feature', () => { - OrganizationStore.onUpdate( - OrganizationFixture({ - features: ['chonk-ui'], - }) - ); - - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(lightTheme); - }); - - it('returns old dark theme if the user prefers chonk theme, but the organization does not have chonk-ui feature', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: true, theme: 'dark'}, - }), - }) - ); - - OrganizationStore.onUpdate( - OrganizationFixture({ - features: [], - }) - ); - - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(darkTheme); - }); - }); - - describe('enabled states', () => { - it('returns light chonk theme if the organization has chonk-ui feature and user prefers chonk theme', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: true, theme: 'system'}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui']})); - - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(DO_NOT_USE_lightChonkTheme); - }); - - it('returns dark chonk theme if the organization has chonk-ui feature and user prefers chonk theme', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: true, theme: 'dark'}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui']})); - - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(DO_NOT_USE_darkChonkTheme); - }); - }); - - describe('enforce states', () => { - it('returns light chonk theme if the organization has chonk-ui-enforce feature and user prefers chonk theme', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: true, theme: 'light'}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui-enforce']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(DO_NOT_USE_lightChonkTheme); - }); - - it('returns dark chonk theme if the organization has chonk-ui-enforce feature and user prefers chonk theme', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: true, theme: 'dark'}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui-enforce']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(DO_NOT_USE_darkChonkTheme); - }); - - it('returns light theme if the organization has chonk-ui-enforce feature and user opted out', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: false, theme: 'light'}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui-enforce']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(lightTheme); - }); - - it('returns chonk theme if the organization has chonk-ui-enforce feature and user has not indicated a preference', () => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: { - ...UserFixture().options, - prefersChonkUI: null, - theme: 'light', - }, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui-enforce']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe(DO_NOT_USE_lightChonkTheme); - }); - - it.each(['light', 'dark', 'system'] as const)( - 'opt-out is respected for opted out users', - theme => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: false, theme}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui-enforce']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe( - theme === 'light' || theme === 'system' ? lightTheme : darkTheme - ); - } - ); - - it.each(['light', 'dark', 'system'] as const)( - 'opt-out is respected for opted out users', - theme => { - ConfigStore.loadInitialData( - ConfigFixture({ - user: UserFixture({ - options: {...UserFixture().options, prefersChonkUI: false, theme}, - }), - }) - ); - OrganizationStore.onUpdate(OrganizationFixture({features: ['chonk-ui']})); - const {result} = renderHookWithProviders(useThemeSwitcher); - expect(result.current).toBe( - theme === 'light' || theme === 'system' ? lightTheme : darkTheme - ); - } - ); - }); -}); diff --git a/static/app/utils/theme/useThemeSwitcher.tsx b/static/app/utils/theme/useThemeSwitcher.tsx deleted file mode 100644 index 09c04898d0c8be..00000000000000 --- a/static/app/utils/theme/useThemeSwitcher.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import {useMemo} from 'react'; -import type {DO_NOT_USE_ChonkTheme, Theme} from '@emotion/react'; - -import {addMessage} from 'sentry/actionCreators/indicator'; -import ConfigStore from 'sentry/stores/configStore'; -import OrganizationStore from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; -import type {User} from 'sentry/types/user'; -import {removeBodyTheme} from 'sentry/utils/removeBodyTheme'; -// eslint-disable-next-line no-restricted-imports -- @TODO(jonasbadalic): Remove theme import -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; -import { - DO_NOT_USE_darkChonkTheme, - DO_NOT_USE_lightChonkTheme, -} from 'sentry/utils/theme/theme.chonk'; -import {useHotkeys} from 'sentry/utils/useHotkeys'; -import useMutateUserOptions from 'sentry/utils/useMutateUserOptions'; -import {useUser} from 'sentry/utils/useUser'; - -export function useThemeSwitcher(): DO_NOT_USE_ChonkTheme | Theme { - const config = useLegacyStore(ConfigStore); - // User can be nullable in some cases where this hook can be called, however the - // type of the user is not nullable, so we will cast it to undefined. - const user = useUser() as User | undefined; - // @TODO(jonasbadalic): the notion of an organization should be removed from the config store - // before release, as we may not always have an organization. When we release, chonk should - // be the value that we receive from the server config - the theme should ultimately be toggled there - const {organization} = useLegacyStore(OrganizationStore); - - const {mutate: mutateUserOptions} = useMutateUserOptions(); - - let theme: Theme | DO_NOT_USE_ChonkTheme = - config.theme === 'dark' ? darkTheme : lightTheme; - - if ( - user && - organization && - ((organization.features.includes('chonk-ui') && user.options.prefersChonkUI) || - (organization.features.includes('chonk-ui-enforce') && - user.options.prefersChonkUI !== false)) - ) { - theme = - config.theme === 'dark' ? DO_NOT_USE_darkChonkTheme : DO_NOT_USE_lightChonkTheme; - } - - // Hotkey definition for toggling the current theme - const themeToggleHotkey = useMemo( - () => ({ - match: ['command+shift+1', 'ctrl+shift+1'], - includeInputs: true, - callback: () => { - removeBodyTheme(); - ConfigStore.set('theme', config.theme === 'dark' ? 'light' : 'dark'); - }, - }), - [config.theme] - ); - - // Hotkey definition for toggling the chonk theme - const chonkThemeToggleHotkey = useMemo( - () => ({ - match: ['command+shift+2', 'ctrl+shift+2'], - includeInputs: true, - callback: () => { - if (user?.options?.prefersChonkUI) { - ConfigStore.set('theme', config.theme); - addMessage(`Using default theme`, 'success'); - mutateUserOptions({prefersChonkUI: false}); - } else { - addMessage(`Previewing new theme`, 'success'); - mutateUserOptions({prefersChonkUI: true}); - } - }, - }), - [user?.options?.prefersChonkUI, config.theme, mutateUserOptions] - ); - - useHotkeys( - organization?.features?.includes('chonk-ui') || - organization?.features?.includes('chonk-ui-enforce') - ? [themeToggleHotkey, chonkThemeToggleHotkey] - : [themeToggleHotkey] - ); - return theme; -} diff --git a/static/app/utils/theme/withChonk.spec.tsx b/static/app/utils/theme/withChonk.spec.tsx index a2624adfdc3565..fbad79d63196ba 100644 --- a/static/app/utils/theme/withChonk.spec.tsx +++ b/static/app/utils/theme/withChonk.spec.tsx @@ -48,18 +48,6 @@ describe('withChonk', () => { OrganizationStore.onUpdate(OrganizationFixture({features: []})); }); - it('renders legacy component when chonk is disabled', () => { - const Component = withChonk(LegacyComponent, ChonkComponent, props => props); - - render( - - - - ); - - expect(screen.getByText(/Legacy: false/)).toBeInTheDocument(); - }); - it('renders chonk component when chonk is enabled', () => { ConfigStore.loadInitialData( ConfigFixture({ @@ -86,23 +74,6 @@ describe('withChonk', () => { expect(screen.getByText(/Chonk: true/)).toBeInTheDocument(); }); - it('passes ref to legacy component', () => { - const ref = createRef(); - const Component = withChonk( - LegacyComponentWithRef, - ChonkComponentWithRef, - props => props - ); - - render( - - - - ); - expect(ref.current).toBeInstanceOf(HTMLDivElement); - expect(screen.getByText(/Legacy: false/)).toBeInTheDocument(); - }); - it('passes ref to chonk component', () => { ConfigStore.loadInitialData( ConfigFixture({ diff --git a/static/app/utils/useMaxPickableDays.tsx b/static/app/utils/useMaxPickableDays.tsx index 6753c1ce74db6c..8e552937f32e9f 100644 --- a/static/app/utils/useMaxPickableDays.tsx +++ b/static/app/utils/useMaxPickableDays.tsx @@ -2,12 +2,29 @@ import {useMemo, type ReactNode} from 'react'; import HookOrDefault from 'sentry/components/hookOrDefault'; import type {DatePageFilterProps} from 'sentry/components/organizations/datePageFilter'; +import {MAX_PICKABLE_DAYS} from 'sentry/constants'; import {t} from 'sentry/locale'; import HookStore from 'sentry/stores/hookStore'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import useOrganization from 'sentry/utils/useOrganization'; +/** + * This returns the default max pickable days for the current organization. + * + * Use this as the default when there is not known data category. + */ +export function useDefaultMaxPickableDays(): number { + const useDefaultMaxPickableDaysHook = + HookStore.get('react-hook:use-default-max-pickable-days')[0] ?? + useDefaultMaxPickableDaysImpl; + return useDefaultMaxPickableDaysHook(); +} + +function useDefaultMaxPickableDaysImpl() { + return MAX_PICKABLE_DAYS; +} + export interface MaxPickableDaysOptions { /** * The maximum number of days the user is allowed to pick on the date page filter diff --git a/static/app/views/dashboards/controls.tsx b/static/app/views/dashboards/controls.tsx index af1799e81b08ba..e4a077e21e1eac 100644 --- a/static/app/views/dashboards/controls.tsx +++ b/static/app/views/dashboards/controls.tsx @@ -87,6 +87,9 @@ function Controls({ const {teams: userTeams} = useUserTeams(); const api = useApi(); const navigate = useNavigate(); + const hasPrebuiltControlsFeature = organization.features.includes( + 'dashboards-prebuilt-controls' + ); const {duplicatePrebuiltDashboard, isLoading: isLoadingDuplicatePrebuiltDashboard} = useDuplicatePrebuiltDashboard({ @@ -97,6 +100,10 @@ function Controls({ const isPrebuiltDashboard = defined(dashboard.prebuiltId); + if (isPrebuiltDashboard && !hasPrebuiltControlsFeature) { + return null; + } + if ([DashboardState.EDIT, DashboardState.PENDING_DELETE].includes(dashboardState)) { return ( diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 4d7a8371e76222..6fa431b29210ba 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -18,6 +18,7 @@ import {IconResize} from 'sentry/icons'; import {t} from 'sentry/locale'; import GroupStore from 'sentry/stores/groupStore'; import {space} from 'sentry/styles/space'; +import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {DatasetSource} from 'sentry/utils/discover/types'; import useApi from 'sentry/utils/useApi'; @@ -430,6 +431,7 @@ function Dashboard({ onSetTransactionsDataset={() => handleChangeSplitDataset(widget, index)} isEmbedded={isEmbedded} isPreview={isPreview} + isPrebuiltDashboard={defined(dashboard.prebuiltId)} dashboardFilters={getDashboardFiltersFromURL(location) ?? dashboard.filters} dashboardPermissions={dashboard.permissions} dashboardCreator={dashboard.createdBy} diff --git a/static/app/views/dashboards/sortableWidget.tsx b/static/app/views/dashboards/sortableWidget.tsx index 8ec30c7a97bb76..0ba30fa95166a3 100644 --- a/static/app/views/dashboards/sortableWidget.tsx +++ b/static/app/views/dashboards/sortableWidget.tsx @@ -42,6 +42,7 @@ type Props = { dashboardPermissions?: DashboardPermissions; isEmbedded?: boolean; isMobile?: boolean; + isPrebuiltDashboard?: boolean; isPreview?: boolean; newlyAddedWidget?: Widget; onNewWidgetScrollComplete?: () => void; @@ -73,18 +74,20 @@ function SortableWidget(props: Props) { newlyAddedWidget, onNewWidgetScrollComplete, useTimeseriesVisualization, + isPrebuiltDashboard = false, } = props; const organization = useOrganization(); const currentUser = useUser(); const {teams: userTeams} = useUserTeams(); - const hasEditAccess = checkUserHasEditAccess( - currentUser, - userTeams, - organization, - dashboardPermissions, - dashboardCreator - ); + const hasEditAccess = + checkUserHasEditAccess( + currentUser, + userTeams, + organization, + dashboardPermissions, + dashboardCreator + ) && !isPrebuiltDashboard; const disableTransactionWidget = organization.features.includes('discover-saved-queries-deprecation') && diff --git a/static/app/views/dashboards/widgetBuilder/widgetLibrary/card.tsx b/static/app/views/dashboards/widgetBuilder/widgetLibrary/card.tsx deleted file mode 100644 index 92d5b211d5e929..00000000000000 --- a/static/app/views/dashboards/widgetBuilder/widgetLibrary/card.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import styled from '@emotion/styled'; - -import {space} from 'sentry/styles/space'; -import type {WidgetTemplate} from 'sentry/views/dashboards/widgetLibrary/data'; -import {getWidgetIcon} from 'sentry/views/dashboards/widgetLibrary/widgetCard'; - -interface CardProps { - iconColor: string; - widget: WidgetTemplate; -} - -export function Card({widget, iconColor}: CardProps) { - const {title, description, displayType} = widget; - const Icon = getWidgetIcon(displayType); - - return ( - - - - - - {title} - {description} - - - ); -} - -const Container = styled('div')` - display: flex; - flex-direction: row; - gap: ${space(1)}; -`; - -const Information = styled('div')` - display: flex; - flex-direction: column; -`; - -const Heading = styled('div')` - font-size: ${p => p.theme.fontSize.lg}; - font-weight: ${p => p.theme.fontWeight.normal}; - margin-bottom: 0; - color: ${p => p.theme.gray500}; -`; - -const SubHeading = styled('small')` - color: ${p => p.theme.subText}; -`; - -const IconWrapper = styled('div')<{backgroundColor: string}>` - display: flex; - justify-content: center; - align-items: center; - padding: ${space(1)}; - min-width: 40px; - height: 40px; - border-radius: ${p => p.theme.borderRadius}; - background: ${p => p.backgroundColor}; -`; diff --git a/static/app/views/dashboards/widgetLibrary/data.tsx b/static/app/views/dashboards/widgetLibrary/data.tsx index 09e9d7d323cdae..79b887dcbe2ff5 100644 --- a/static/app/views/dashboards/widgetLibrary/data.tsx +++ b/static/app/views/dashboards/widgetLibrary/data.tsx @@ -6,7 +6,7 @@ import type {Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; -export type WidgetTemplate = Widget & { +type WidgetTemplate = Widget & { description: string; }; diff --git a/static/app/views/detectors/components/details/metric/chart.tsx b/static/app/views/detectors/components/details/metric/chart.tsx index 590757443868aa..d636a756075a9e 100644 --- a/static/app/views/detectors/components/details/metric/chart.tsx +++ b/static/app/views/detectors/components/details/metric/chart.tsx @@ -30,6 +30,7 @@ import { useIncidentMarkers, type IncidentPeriod, } from 'sentry/views/detectors/hooks/useIncidentMarkers'; +import {useMetricDetectorAnomalyThresholds} from 'sentry/views/detectors/hooks/useMetricDetectorAnomalyThresholds'; import {useMetricDetectorSeries} from 'sentry/views/detectors/hooks/useMetricDetectorSeries'; import {useMetricDetectorThresholdSeries} from 'sentry/views/detectors/hooks/useMetricDetectorThresholdSeries'; import {useOpenPeriods} from 'sentry/views/detectors/hooks/useOpenPeriods'; @@ -156,6 +157,7 @@ export function useMetricDetectorChart({ }: UseMetricDetectorChartProps): UseMetricDetectorChartResult { const navigate = useNavigate(); const location = useLocation(); + const detectionType = detector.config.detectionType; const comparisonDelta = detectionType === 'percent' ? detector.config.comparisonDelta : undefined; @@ -179,6 +181,34 @@ export function useMetricDetectorChart({ end, }); + const metricTimestamps = useMemo(() => { + const firstSeries = series[0]; + if (!firstSeries?.data.length) { + return {start: undefined, end: undefined}; + } + const data = firstSeries.data; + const firstPoint = data[0]; + const lastPoint = data[data.length - 1]; + + if (!firstPoint || !lastPoint) { + return {start: undefined, end: undefined}; + } + + const firstTimestamp = + typeof firstPoint.name === 'number' + ? firstPoint.name + : new Date(firstPoint.name).getTime(); + const lastTimestamp = + typeof lastPoint.name === 'number' + ? lastPoint.name + : new Date(lastPoint.name).getTime(); + + return { + start: Math.floor(firstTimestamp / 1000), + end: Math.floor(lastTimestamp / 1000), + }; + }, [series]); + const {maxValue: thresholdMaxValue, additionalSeries: thresholdAdditionalSeries} = useMetricDetectorThresholdSeries({ conditions: detector.conditionGroup?.conditions, @@ -187,6 +217,13 @@ export function useMetricDetectorChart({ comparisonSeries, }); + const {anomalyThresholdSeries} = useMetricDetectorAnomalyThresholds({ + detectorId: detector.id, + startTimestamp: metricTimestamps.start, + endTimestamp: metricTimestamps.end, + series, + }); + const incidentPeriods = useMemo(() => { return openPeriods.flatMap(period => [ createTriggerIntervalMarkerData({ @@ -227,13 +264,17 @@ export function useMetricDetectorChart({ const {maxValue, minValue} = useDetectorChartAxisBounds({series, thresholdMaxValue}); const additionalSeries = useMemo(() => { - const baseSeries = [...thresholdAdditionalSeries]; + const baseSeries = [...thresholdAdditionalSeries, ...anomalyThresholdSeries]; // Line series not working well with the custom series type baseSeries.push(openPeriodMarkerResult.incidentMarkerSeries as any); return baseSeries; - }, [thresholdAdditionalSeries, openPeriodMarkerResult.incidentMarkerSeries]); + }, [ + thresholdAdditionalSeries, + anomalyThresholdSeries, + openPeriodMarkerResult.incidentMarkerSeries, + ]); const yAxes = useMemo(() => { const {formatYAxisLabel, outputType} = getDetectorChartFormatters({ diff --git a/static/app/views/detectors/hooks/useMetricDetectorAnomalyThresholds.tsx b/static/app/views/detectors/hooks/useMetricDetectorAnomalyThresholds.tsx new file mode 100644 index 00000000000000..7b3e57453270fa --- /dev/null +++ b/static/app/views/detectors/hooks/useMetricDetectorAnomalyThresholds.tsx @@ -0,0 +1,173 @@ +import {useMemo} from 'react'; +import {useTheme} from '@emotion/react'; +import type {LineSeriesOption} from 'echarts'; + +import LineSeries from 'sentry/components/charts/series/lineSeries'; +import type {Series} from 'sentry/types/echarts'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import type RequestError from 'sentry/utils/requestError/requestError'; +import useOrganization from 'sentry/utils/useOrganization'; + +interface AnomalyThresholdDataPoint { + external_alert_id: number; + timestamp: number; + value: number; + yhat_lower: number; + yhat_upper: number; +} + +interface AnomalyThresholdDataResponse { + data: AnomalyThresholdDataPoint[]; +} + +interface UseMetricDetectorAnomalyThresholdsProps { + detectorId: string; + endTimestamp?: number; + series?: Series[]; + startTimestamp?: number; +} + +interface UseMetricDetectorAnomalyThresholdsResult { + anomalyThresholdSeries: LineSeriesOption[]; + error: RequestError | null; + isLoading: boolean; +} + +/** + * Fetches anomaly detection threshold data and transforms it into chart series + */ +export function useMetricDetectorAnomalyThresholds({ + detectorId, + startTimestamp, + endTimestamp, + series = [], +}: UseMetricDetectorAnomalyThresholdsProps): UseMetricDetectorAnomalyThresholdsResult { + const organization = useOrganization(); + const theme = useTheme(); + + const hasAnomalyDataFlag = organization.features.includes( + 'anomaly-detection-threshold-data' + ); + + const { + data: anomalyData, + isLoading, + error, + } = useApiQuery( + [ + `/organizations/${organization.slug}/detectors/${detectorId}/anomaly-data/`, + { + query: { + start: startTimestamp, + end: endTimestamp, + }, + }, + ], + { + staleTime: 0, + enabled: + hasAnomalyDataFlag && Boolean(detectorId && startTimestamp && endTimestamp), + } + ); + + const anomalyThresholdSeries = useMemo(() => { + if (!anomalyData?.data || anomalyData.data.length === 0 || series.length === 0) { + return []; + } + + const data = anomalyData.data; + const metricData = series[0]?.data; + + if (!metricData || metricData.length === 0) { + return []; + } + + const anomalyMap = new Map(data.map(point => [point.timestamp * 1000, point])); + + const upperBoundData: Array<[number, number]> = []; + const lowerBoundData: Array<[number, number]> = []; + const seerValueData: Array<[number, number]> = []; + + metricData.forEach(metricPoint => { + const timestamp = + typeof metricPoint.name === 'number' + ? metricPoint.name + : new Date(metricPoint.name).getTime(); + const anomalyPoint = anomalyMap.get(timestamp); + + if (anomalyPoint) { + upperBoundData.push([timestamp, anomalyPoint.yhat_upper]); + lowerBoundData.push([timestamp, anomalyPoint.yhat_lower]); + seerValueData.push([timestamp, anomalyPoint.value]); + } + }); + + const lineColor = theme.red300; + const seerValueColor = theme.yellow300; + + return [ + LineSeries({ + name: 'Upper Threshold', + data: upperBoundData, + lineStyle: { + color: lineColor, + type: 'dashed', + width: 1, + dashOffset: 0, + }, + areaStyle: { + color: lineColor, + opacity: 0.05, + origin: 'end', + }, + itemStyle: {color: lineColor}, + animation: false, + animationThreshold: 1, + animationDuration: 0, + symbol: 'none', + connectNulls: true, + step: false, + }), + LineSeries({ + name: 'Lower Threshold', + data: lowerBoundData, + lineStyle: { + color: lineColor, + type: 'dashed', + width: 1, + dashOffset: 0, + }, + areaStyle: { + color: lineColor, + opacity: 0.05, + origin: 'start', + }, + itemStyle: {color: lineColor}, + animation: false, + animationThreshold: 1, + animationDuration: 0, + symbol: 'none', + connectNulls: true, + step: false, + }), + LineSeries({ + name: 'Seer Historical Value', + data: seerValueData, + lineStyle: { + color: seerValueColor, + type: 'solid', + width: 2, + }, + itemStyle: {color: seerValueColor}, + animation: false, + animationThreshold: 1, + animationDuration: 0, + symbol: 'circle', + symbolSize: 4, + connectNulls: true, + }), + ]; + }, [anomalyData, series, theme]); + + return {anomalyThresholdSeries, isLoading, error}; +} diff --git a/static/app/views/explore/components/attributeBreakdowns/attributeComparisonCTA.tsx b/static/app/views/explore/components/attributeBreakdowns/attributeComparisonCTA.tsx deleted file mode 100644 index 6396fbc6ffe56d..00000000000000 --- a/static/app/views/explore/components/attributeBreakdowns/attributeComparisonCTA.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; - -import emptyTraceImg from 'sentry-images/spot/performance-empty-trace.svg'; - -import {Flex} from '@sentry/scraps/layout/flex'; -import {Text} from '@sentry/scraps/text'; - -import {Button} from 'sentry/components/core/button'; -import Panel from 'sentry/components/panels/panel'; -import {IconClose} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {useDebouncedValue} from 'sentry/utils/useDebouncedValue'; -import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; -import {Tab, useTab} from 'sentry/views/explore/hooks/useTab'; - -import {useChartSelection} from './chartSelectionContext'; - -const PANEL_WIDTH = '400px'; -const ILLUSTRATION_HEIGHT = '120px'; -const LOCAL_STORAGE_KEY = 'explore:attribute-breakdowns-cta-dismissed'; -const CTA_DELAY_MS = 1000; - -export function AttributeComparisonCTA({children}: {children: React.ReactNode}) { - const [tab] = useTab(); - const {chartSelection} = useChartSelection(); - const [isDismissed, setIsDismissed] = useLocalStorageState( - LOCAL_STORAGE_KEY, - false - ); - - const isAttributeBreakdownsTab = tab === Tab.ATTRIBUTE_BREAKDOWNS; - const showCTA = !chartSelection && !isDismissed && isAttributeBreakdownsTab; - - // Adding a delay so the CTA appears after we have switched - // to the attribute breakdowns tab and draws attention - const showDelayed = useDebouncedValue(showCTA, CTA_DELAY_MS); - - return ( - - {children} - {showDelayed && showCTA ? ( - - - - - {t('Examine what sets your selection apart')} - - - + ); } @@ -346,7 +376,7 @@ function DynamicGrouping() { const {teams: userTeams} = useUserTeams(); const [filterByAssignedToMe, setFilterByAssignedToMe] = useState(true); const [selectedTeamIds, setSelectedTeamIds] = useState>(new Set()); - const [minFixabilityScore, setMinFixabilityScore] = useState(50); + const [selectedTags, setSelectedTags] = useState>(new Set()); const [removedClusterIds, setRemovedClusterIds] = useState(new Set()); const [showJsonInput, setShowJsonInput] = useState(false); const [jsonInputValue, setJsonInputValue] = useState(''); @@ -429,6 +459,43 @@ function DynamicGrouping() { setRemovedClusterIds(prev => new Set([...prev, clusterId])); }; + const handleTagClick = (tag: string) => { + setSelectedTags(prev => { + const next = new Set(prev); + if (next.has(tag)) { + next.delete(tag); + } else { + next.add(tag); + } + return next; + }); + }; + + const handleClearTagFilter = (tag: string) => { + setSelectedTags(prev => { + const next = new Set(prev); + next.delete(tag); + return next; + }); + }; + + const handleClearAllTagFilters = () => { + setSelectedTags(new Set()); + }; + + // Helper to check if a cluster has any of the selected tags + const clusterHasSelectedTags = (cluster: ClusterSummary): boolean => { + if (selectedTags.size === 0) return true; + + const allClusterTags = [ + ...(cluster.service_tags ?? []), + ...(cluster.error_type_tags ?? []), + ...(cluster.code_area_tags ?? []), + ]; + + return Array.from(selectedTags).every(tag => allClusterTags.includes(tag)); + }; + // When using custom JSON data with filters disabled, skip all filtering and sorting const shouldSkipFilters = isUsingCustomData && disableFilters; const filteredAndSortedClusters = shouldSkipFilters @@ -437,8 +504,8 @@ function DynamicGrouping() { .filter(cluster => { if (removedClusterIds.has(cluster.cluster_id)) return false; - const fixabilityScore = (cluster.fixability_score ?? 0) * 100; - if (fixabilityScore < minFixabilityScore) return false; + // Filter by selected tags + if (!clusterHasSelectedTags(cluster)) return false; if (filterByAssignedToMe) { if (!cluster.assignedTo?.length) return false; @@ -560,6 +627,31 @@ function DynamicGrouping() { {shouldSkipFilters && ` ${t('(filters disabled)')}`} + {selectedTags.size > 0 && ( + + + {t('Filtering by tags:')} + + + {Array.from(selectedTags).map(tag => ( + + {tag} + + + + )} + {!shouldSkipFilters && ( )} - - - - {t('Minimum fixability score (%)')} - - setMinFixabilityScore(value ?? 0)} - aria-label={t('Minimum fixability score')} - size="sm" - /> - @@ -651,6 +729,8 @@ function DynamicGrouping() { key={cluster.cluster_id} cluster={cluster} onRemove={handleRemoveCluster} + onTagClick={handleTagClick} + selectedTags={selectedTags} /> ))} @@ -686,63 +766,127 @@ const CardsGrid = styled('div')` } `; -// Card with hover effect +// Card with subtle hover effect const CardContainer = styled('div')` background: ${p => p.theme.background}; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; - padding: ${space(3)}; display: flex; flex-direction: column; min-width: 0; + overflow: hidden; transition: border-color 0.2s ease, box-shadow 0.2s ease; &:hover { - border-color: ${p => p.theme.purple300}; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: ${p => p.theme.purple200}; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } `; -// Issue count badge - compact version -const IssueCountBadge = styled('div')` +// Zone 1: Title area - clean and prominent +const CardHeader = styled('div')` + padding: ${space(3)} ${space(3)} ${space(2)}; display: flex; flex-direction: column; - align-items: center; - padding: ${space(1)} ${space(1.5)}; - background: ${p => p.theme.purple100}; - border-radius: ${p => p.theme.borderRadius}; - flex-shrink: 0; + gap: ${space(1)}; `; -const IssueCountNumber = styled('div')` - font-size: 24px; +const ClusterTitle = styled('h3')` + margin: 0; + font-size: ${p => p.theme.fontSize.xl}; font-weight: 600; - color: ${p => p.theme.purple400}; - line-height: 1; + color: ${p => p.theme.textColor}; + line-height: 1.3; + word-break: break-word; `; -// Horizontal stats bar below header -const ClusterStatsBar = styled('div')` +// Zone 2: Stats section with visual hierarchy +const StatsSection = styled('div')` + padding: ${space(2)} ${space(3)}; + background: ${p => p.theme.backgroundSecondary}; + border-top: 1px solid ${p => p.theme.innerBorder}; + border-bottom: 1px solid ${p => p.theme.innerBorder}; display: flex; - flex-wrap: wrap; + justify-content: space-between; align-items: center; gap: ${space(2)}; - padding: ${space(1.5)} 0; - margin-top: ${space(1.5)}; - border-top: 1px solid ${p => p.theme.innerBorder}; + flex-wrap: wrap; +`; + +const PrimaryStats = styled('div')` + display: flex; + align-items: center; + gap: ${space(3)}; +`; + +const EventsMetric = styled('div')` + display: flex; + align-items: center; + gap: ${space(1)}; + color: ${p => p.theme.red300}; +`; + +const EventsCount = styled('span')` + font-size: ${p => p.theme.fontSize.xl}; + font-weight: 700; + color: ${p => p.theme.textColor}; + font-variant-numeric: tabular-nums; +`; + +const SecondaryStats = styled('div')` + display: flex; + gap: ${space(3)}; +`; + +const SecondaryStatItem = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(0.25)}; font-size: ${p => p.theme.fontSize.sm}; + color: ${p => p.theme.textColor}; +`; + +// Zone 3: Issues list with clear containment +const IssuesSection = styled('div')` + padding: ${space(2)} ${space(3)}; + flex: 1; + display: flex; + flex-direction: column; +`; + +const IssuesSectionHeader = styled('div')` + margin-bottom: ${space(1.5)}; color: ${p => p.theme.subText}; + letter-spacing: 0.5px; `; -const StatItem = styled('div')` +const IssuesList = styled('div')` display: flex; - align-items: center; - gap: ${space(0.5)}; + flex-direction: column; + gap: ${space(1.5)}; +`; + +const MoreIssuesIndicator = styled('div')` + font-size: ${p => p.theme.fontSize.sm}; + color: ${p => p.theme.subText}; + text-align: center; + font-style: italic; + padding-top: ${space(1)}; +`; + +// Zone 4: Footer with actions +const CardFooter = styled('div')` + padding: ${space(2)} ${space(3)}; + border-top: 1px solid ${p => p.theme.innerBorder}; + display: flex; + justify-content: flex-end; + gap: ${space(1)}; + background: ${p => p.theme.backgroundSecondary}; `; -// Issue preview link with hover effect +// Issue preview link with hover effect - consistent with issue feed cards const IssuePreviewLink = styled(Link)` display: block; padding: ${space(1.5)} ${space(2)}; @@ -762,7 +906,7 @@ const IssuePreviewLink = styled(Link)` // Issue title with ellipsis and nested em styling for EventOrGroupTitle const IssueTitle = styled('div')` font-size: ${p => p.theme.fontSize.md}; - font-weight: ${p => p.theme.fontWeight.bold}; + font-weight: 600; color: ${p => p.theme.textColor}; line-height: 1.4; ${p => p.theme.overflowEllipsis}; @@ -780,6 +924,7 @@ const IssueMessage = styled(EventMessage)` margin: 0; font-size: ${p => p.theme.fontSize.sm}; color: ${p => p.theme.subText}; + opacity: 0.9; `; // Meta separator line @@ -835,4 +980,53 @@ const CustomDataBadge = styled('div')` color: ${p => p.theme.yellow400}; `; +const ClickableTag = styled(Tag)<{isSelected?: boolean}>` + cursor: pointer; + transition: + background 0.15s ease, + border-color 0.15s ease, + transform 0.1s ease, + box-shadow 0.15s ease; + user-select: none; + + ${p => + p.isSelected && + ` + background: ${p.theme.purple100}; + border-color: ${p.theme.purple300}; + color: ${p.theme.purple400}; + `} + + &:hover { + background: ${p => (p.isSelected ? p.theme.purple200 : p.theme.gray100)}; + border-color: ${p => (p.isSelected ? p.theme.purple400 : p.theme.gray300)}; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active { + transform: translateY(0); + box-shadow: none; + } +`; + +const ActiveTagFilters = styled('div')` + display: flex; + align-items: center; + gap: ${space(1)}; + margin-top: ${space(1.5)}; + flex-wrap: wrap; +`; + +const ActiveTagChip = styled('div')` + display: flex; + align-items: center; + gap: ${space(0.5)}; + padding: ${space(0.25)} ${space(0.5)} ${space(0.25)} ${space(1)}; + background: ${p => p.theme.purple100}; + border: 1px solid ${p => p.theme.purple200}; + border-radius: ${p => p.theme.borderRadius}; + color: ${p => p.theme.purple400}; +`; + export default DynamicGrouping; diff --git a/static/app/views/nav/index.spec.tsx b/static/app/views/nav/index.spec.tsx index 3557b2c4e153d1..e6e114cc2f546d 100644 --- a/static/app/views/nav/index.spec.tsx +++ b/static/app/views/nav/index.spec.tsx @@ -514,181 +514,4 @@ describe('Nav', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); - - describe('chonk-ui', () => { - describe('switching themes', () => { - beforeEach(() => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - - ConfigStore.set('user', { - ...ConfigStore.get('user'), - options: { - ...ConfigStore.get('user').options, - prefersChonkUI: false, - }, - }); - }); - - describe('when feature flag is enabled', () => { - it('shows the chonk-ui toggle in the help menu', async () => { - const dismissRequest = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - method: 'PUT', - }); - - renderNav({features: ALL_AVAILABLE_FEATURES.concat('chonk-ui')}); - const helpMenu = screen.getByRole('button', {name: 'Help'}); - await userEvent.click(helpMenu); - - expect(screen.getByText('Try our new look')).toBeInTheDocument(); - - // Once for banner, once for dot indicator - expect(dismissRequest).toHaveBeenCalledTimes(2); - }); - - it('shows the chonk-ui toggle to old theme', async () => { - ConfigStore.set('user', { - ...ConfigStore.get('user'), - options: { - ...ConfigStore.get('user').options, - prefersChonkUI: true, - }, - }); - - renderNav({features: ALL_AVAILABLE_FEATURES.concat('chonk-ui')}); - const helpMenu = screen.getByRole('button', {name: 'Help'}); - await userEvent.click(helpMenu); - - expect(screen.getByText('Switch back to our old look')).toBeInTheDocument(); - }); - }); - - describe('when feature flag is disabled', () => { - it('does not show the chonk-ui toggle in the help menu', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - - renderNav({features: ALL_AVAILABLE_FEATURES}); - const helpMenu = screen.getByRole('button', {name: 'Help'}); - await userEvent.click(helpMenu); - - expect(screen.queryByText('Try our new look')).not.toBeInTheDocument(); - }); - }); - }); - - describe('opt-in banner', () => { - it('shows the opt-in banner if user has feature and has not opted in yet', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - - renderNav({features: ALL_AVAILABLE_FEATURES.concat('chonk-ui')}); - expect(await screen.findByText(/Sentry has a new look/)).toBeInTheDocument(); - }); - - it('dismissing the banner', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - - const dismissRequest = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - method: 'PUT', - status: 200, - }); - - renderNav({features: ALL_AVAILABLE_FEATURES.concat('chonk-ui')}); - expect(await screen.findByText(/Sentry has a new look/)).toBeInTheDocument(); - - await userEvent.click(screen.getByRole('button', {name: 'Dismiss'})); - - expect(dismissRequest).toHaveBeenCalled(); - - await waitFor(() => { - expect(dismissRequest).toHaveBeenCalledWith( - '/organizations/org-slug/prompts-activity/', - expect.objectContaining({ - method: 'PUT', - data: expect.objectContaining({ - feature: 'chonk_ui_banner', - status: 'snoozed', - }), - }) - ); - }); - - expect(screen.queryByText(/Sentry has a new look/)).not.toBeInTheDocument(); - }); - - it('enabling new theme dismisses banner and dot indicator', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - - const optInRequest = MockApiClient.addMockResponse({ - url: '/users/me/', - method: 'PUT', - }); - - const dismissRequest = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - method: 'PUT', - }); - - renderNav({features: ALL_AVAILABLE_FEATURES.concat('chonk-ui')}); - expect(await screen.findByText(/Sentry has a new look/)).toBeInTheDocument(); - - // Enables user option and disables all prompts - await userEvent.click(screen.getByText('Try It Out')); - - expect(optInRequest).toHaveBeenCalledWith( - '/users/me/', - expect.objectContaining({ - method: 'PUT', - data: expect.objectContaining({ - options: expect.objectContaining({ - prefersChonkUI: true, - }), - }), - }) - ); - - expect(dismissRequest).toHaveBeenNthCalledWith( - 1, - '/organizations/org-slug/prompts-activity/', - expect.objectContaining({ - method: 'PUT', - data: expect.objectContaining({ - feature: 'chonk_ui_banner', - status: 'snoozed', - }), - }) - ); - - expect(dismissRequest).toHaveBeenNthCalledWith( - 2, - '/organizations/org-slug/prompts-activity/', - expect.objectContaining({ - method: 'PUT', - data: expect.objectContaining({ - feature: 'chonk_ui_dot_indicator', - status: 'snoozed', - }), - }) - ); - - // The banner is no longer visible - expect(screen.queryByText(/Sentry has a new look/)).not.toBeInTheDocument(); - }); - }); - }); }); diff --git a/static/app/views/nav/primary/help.tsx b/static/app/views/nav/primary/help.tsx index f307cca21f7132..185c14caf56ac3 100644 --- a/static/app/views/nav/primary/help.tsx +++ b/static/app/views/nav/primary/help.tsx @@ -1,7 +1,4 @@ -import {Fragment} from 'react'; - import {openHelpSearchModal} from 'sentry/actionCreators/modal'; -import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {IconQuestion} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -10,9 +7,7 @@ import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useChonkPrompt} from 'sentry/utils/theme/useChonkPrompt'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; -import useMutateUserOptions from 'sentry/utils/useMutateUserOptions'; import useOrganization from 'sentry/utils/useOrganization'; -import {useUser} from 'sentry/utils/useUser'; import {activateZendesk, hasZendesk} from 'sentry/utils/zendesk'; import {SidebarMenu} from 'sentry/views/nav/primary/components'; import { @@ -54,8 +49,6 @@ function getContactSupportItem({ export function PrimaryNavigationHelp() { const organization = useOrganization(); - const user = useUser(); - const {mutate: mutateUserOptions} = useMutateUserOptions(); const contactSupportItem = getContactSupportItem({organization}); const openForm = useFeedbackForm(); const {startTour} = useStackedNavigationTour(); @@ -131,38 +124,6 @@ export function PrimaryNavigationHelp() { startTour(); }, }, - organization?.features?.includes('chonk-ui') || - organization?.features?.includes('chonk-ui-enforce') - ? user.options.prefersChonkUI || - // Show opt-out if the user has not indicated a preference and enforce flag is enabled - (user.options.prefersChonkUI === null && - organization?.features?.includes('chonk-ui-enforce')) - ? { - key: 'old-chonk-ui', - label: t('Switch back to our old look'), - onAction() { - mutateUserOptions({prefersChonkUI: false}); - trackAnalytics('navigation.help_menu_opt_out_chonk_ui_clicked', { - organization, - }); - }, - } - : { - key: 'new-chonk-ui', - label: ( - - {t('Try our new look')} - - ), - textValue: 'Try our new look', - onAction() { - mutateUserOptions({prefersChonkUI: true}); - trackAnalytics('navigation.help_menu_opt_in_chonk_ui_clicked', { - organization, - }); - }, - } - : null, ].filter(n => !!n), }, ]} diff --git a/static/app/views/nav/primary/index.tsx b/static/app/views/nav/primary/index.tsx index 7f364191ec7a2e..6b81812fccf804 100644 --- a/static/app/views/nav/primary/index.tsx +++ b/static/app/views/nav/primary/index.tsx @@ -15,7 +15,6 @@ import { IconSettings, IconSiren, } from 'sentry/icons'; -import {ChonkOptInBanner} from 'sentry/utils/theme/ChonkOptInBanner'; import useOrganization from 'sentry/utils/useOrganization'; import {getDefaultExploreRoute} from 'sentry/views/explore/utils'; import {useNavContext} from 'sentry/views/nav/context'; @@ -194,7 +193,6 @@ export function PrimaryNavigationItems() { - diff --git a/static/app/views/nav/secondary/secondary.tsx b/static/app/views/nav/secondary/secondary.tsx index df1e5b8b72f866..2f473b30c60f01 100644 --- a/static/app/views/nav/secondary/secondary.tsx +++ b/static/app/views/nav/secondary/secondary.tsx @@ -278,6 +278,8 @@ const Header = styled('div')` font-size: ${p => p.theme.fontSize.md}; font-weight: ${p => p.theme.fontWeight.bold}; padding: 0 ${space(1)} 0 ${space(2)}; + + /* This is used in detail pages to match the height of sidebar header. */ height: 44px; border-bottom: 1px solid ${p => p.theme.innerBorder}; diff --git a/static/app/views/nav/secondary/secondarySidebar.tsx b/static/app/views/nav/secondary/secondarySidebar.tsx index 7167833ddfb106..a10b557afe54ec 100644 --- a/static/app/views/nav/secondary/secondarySidebar.tsx +++ b/static/app/views/nav/secondary/secondarySidebar.tsx @@ -87,7 +87,7 @@ export function SecondarySidebar() { } const SecondarySidebarWrapper = styled(NavTourElement)` - background: ${p => (p.theme.isChonk ? p.theme.background : p.theme.surface200)}; + background: ${p => p.theme.backgroundSecondary}; border-right: 1px solid ${p => (p.theme.isChonk ? p.theme.border : p.theme.translucentGray200)}; position: relative; diff --git a/static/app/views/nav/secondary/sections/issues/issueViews/useStarredIssueViews.tsx b/static/app/views/nav/secondary/sections/issues/issueViews/useStarredIssueViews.tsx index 17f5ae0c7c2acc..bedb517643d1b7 100644 --- a/static/app/views/nav/secondary/sections/issues/issueViews/useStarredIssueViews.tsx +++ b/static/app/views/nav/secondary/sections/issues/issueViews/useStarredIssueViews.tsx @@ -11,7 +11,7 @@ export function useStarredIssueViews() { const organization = useOrganization(); const queryClient = useQueryClient(); - const {data: groupSearchViews} = useApiQuery( + const {data: groupSearchViews} = useApiQuery>( makeFetchStarredGroupSearchViewsKey({orgSlug: organization.slug}), {notifyOnChangeProps: ['data'], staleTime: 0} ); @@ -21,7 +21,9 @@ export function useStarredIssueViews() { // XXX (malwilley): Issue views without the nav require at least one issue view, // so they respond with "fake" issue views that do not have an ID. // We should remove this from the backend and here once we remove the tab-based views. - ?.filter(view => defined(view.id)) + ?.filter( + (view): view is StarredGroupSearchView => defined(view) && defined(view.id) + ) .map(convertGSVtoIssueView) ?? []; const setStarredIssueViews = useCallback( diff --git a/static/app/views/nav/secondary/sections/issues/issuesSecondaryNav.tsx b/static/app/views/nav/secondary/sections/issues/issuesSecondaryNav.tsx index 3d4fbcd77770b7..8a3c3db79764ef 100644 --- a/static/app/views/nav/secondary/sections/issues/issuesSecondaryNav.tsx +++ b/static/app/views/nav/secondary/sections/issues/issuesSecondaryNav.tsx @@ -112,6 +112,6 @@ const StickyBottomSection = styled(SecondaryNav.Section, { position: sticky; bottom: 0; z-index: 1; - background: ${p.theme.isChonk ? p.theme.background : p.theme.surface200}; + background: ${p.theme.backgroundSecondary}; `} `; diff --git a/static/app/views/replays/detail/ai/ai.tsx b/static/app/views/replays/detail/ai/ai.tsx index 1a5e42ecc2d445..35df24da5e58c9 100644 --- a/static/app/views/replays/detail/ai/ai.tsx +++ b/static/app/views/replays/detail/ai/ai.tsx @@ -42,7 +42,7 @@ export default function Ai() { const skipConsentFlow = organization.features.includes('gen-ai-consent-flow-removal'); const replayTooLongMessage = t( - 'While in beta phase, we only summarize a small portion of the replay.' + 'If a replay is too long, we may only summarize a small portion of it.' ); const { diff --git a/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx b/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx index fa569e3e657109..ffd2e5cc15346a 100644 --- a/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx +++ b/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx @@ -20,11 +20,11 @@ export default function ReplayDetailsHeaderActions({readerResult}: Props) { renderArchived={() => null} renderError={() => null} renderThrottled={() => null} - renderLoading={() => } + renderLoading={() => } renderMissing={() => null} renderProcessingError={({replayRecord, projectSlug}) => ( - + {({replay}) => ( - + null} renderError={() => null} renderThrottled={() => null} - renderLoading={() => } + renderLoading={() => ( + + + + )} renderMissing={() => null} renderProcessingError={() => null} > diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx index e7b192d582f953..ceee0401f41b8d 100644 --- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx +++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx @@ -186,6 +186,7 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) { const StyledBreadcrumbs = styled(Breadcrumbs)` padding: 0; + height: 34px; `; const ShortId = styled('div')` diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx index dd1e201ffe1549..2cac2707ef7728 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx @@ -1,30 +1,19 @@ import styled from '@emotion/styled'; -import {Flex} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; - import {Button} from 'sentry/components/core/button'; -import {Link} from 'sentry/components/core/link'; -import UserBadge from 'sentry/components/idBadge/userBadge'; -import * as Layout from 'sentry/components/layouts/thirds'; +import {Flex} from 'sentry/components/core/layout'; import Placeholder from 'sentry/components/placeholder'; import ReplayLoadingState from 'sentry/components/replays/player/replayLoadingState'; -import { - getLiveDurationMs, - getReplayExpiresAtMs, - LIVE_TOOLTIP_MESSAGE, - LiveIndicator, -} from 'sentry/components/replays/replayLiveIndicator'; -import TimeSince from 'sentry/components/timeSince'; -import {IconCalendar, IconRefresh} from 'sentry/icons'; +import {getReplayExpiresAtMs} from 'sentry/components/replays/replayLiveIndicator'; +import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns'; +import {IconRefresh} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useQueryClient} from 'sentry/utils/queryClient'; import type useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import usePollReplayRecord from 'sentry/utils/replays/hooks/usePollReplayRecord'; import {useReplayProjectSlug} from 'sentry/utils/replays/hooks/useReplayProjectSlug'; +import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useReplaySummaryContext} from 'sentry/views/replays/detail/ai/replaySummaryContext'; import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; @@ -32,7 +21,6 @@ import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; interface Props { readerResult: ReturnType; } - export default function ReplayDetailsUserBadge({readerResult}: Props) { const organization = useOrganization(); const replayRecord = readerResult.replayRecord; @@ -58,11 +46,8 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { } return null; }; - const searchQuery = getUserSearchQuery(); - const userDisplayName = replayRecord?.user.display_name || t('Anonymous User'); const projectSlug = useReplayProjectSlug({replayRecord}); - const {startSummaryRequest} = useReplaySummaryContext(); const handleRefresh = async () => { @@ -99,77 +84,46 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { const showRefreshButton = polledCountSegments > prevSegments; - const showLiveIndicator = - !isReplayExpired && replayRecord && getLiveDurationMs(replayRecord.finished_at) > 0; + const location = useLocation(); + const linkQuery = searchQuery + ? { + pathname: makeReplaysPathname({ + path: '/', + organization, + }), + query: { + query: searchQuery, + }, + } + : { + pathname: makeReplaysPathname({ + path: `/${replayId}/`, + organization, + }), + query: { + ...location.query, + }, + }; const badge = replayRecord ? ( - - - {searchQuery ? ( - - {userDisplayName} - - ) : ( - userDisplayName - )} - - {replayRecord.started_at ? ( - - - - {showLiveIndicator ? ( - - - - {t('LIVE')} - - - - - ) : null} - - - ) : null} - - } - user={{ - name: replayRecord.user.display_name || '', - email: replayRecord.user.email || '', - username: replayRecord.user.username || '', - ip_address: replayRecord.user.ip || '', - id: replayRecord.user.id || '', - }} - hideEmail - /> + + + + ) : null; return ( @@ -179,7 +133,7 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { renderError={() => null} renderThrottled={() => null} renderLoading={() => - replayRecord ? badge : + replayRecord ? badge : } renderMissing={() => null} renderProcessingError={() => badge} @@ -189,16 +143,11 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { ); } -const TimeContainer = styled('div')` - display: flex; - gap: ${space(1)}; - align-items: center; - color: ${p => p.theme.subText}; - font-size: ${p => p.theme.fontSize.md}; - line-height: 1.4; +// column components expect to be stored in a relative container +const ColumnWrapper = styled(Flex)` + position: relative; `; -const DisplayHeader = styled('div')` - display: flex; - flex-direction: column; +const StyledReplaySessionColumn = styled(ReplaySessionColumn.Component)` + flex: 0; `; diff --git a/static/app/views/replays/detail/header/replayItemDropdown.tsx b/static/app/views/replays/detail/header/replayItemDropdown.tsx index a9d98daca4417f..9a918f2281c85c 100644 --- a/static/app/views/replays/detail/header/replayItemDropdown.tsx +++ b/static/app/views/replays/detail/header/replayItemDropdown.tsx @@ -139,7 +139,7 @@ export default function ReplayItemDropdown({projectSlug, replay, replayRecord}: showChevron: false, icon: , }} - size="sm" + size="xs" items={dropdownItems} isDisabled={dropdownItems.length === 0} /> diff --git a/static/app/views/replays/detail/layout/focusTabs.tsx b/static/app/views/replays/detail/layout/focusTabs.tsx index 7e18e52e741398..cc0923756ffea5 100644 --- a/static/app/views/replays/detail/layout/focusTabs.tsx +++ b/static/app/views/replays/detail/layout/focusTabs.tsx @@ -51,7 +51,7 @@ function getReplayTabs({ > {t('AI Summary')} - + ) : null, [TabKey.BREADCRUMBS]: t('Breadcrumbs'), diff --git a/static/app/views/replays/details.tsx b/static/app/views/replays/details.tsx index eee7adeaf6bd5f..eb3a0f9a02ff91 100644 --- a/static/app/views/replays/details.tsx +++ b/static/app/views/replays/details.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import invariant from 'invariant'; import AnalyticsArea from 'sentry/components/analyticsArea'; +import {Flex} from 'sentry/components/core/layout'; import FullViewport from 'sentry/components/layouts/fullViewport'; import * as Layout from 'sentry/components/layouts/thirds'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; @@ -55,7 +56,21 @@ export default function ReplayDetails() { ? `${replayRecord.user.display_name ?? t('Anonymous User')} — Session Replay — ${orgSlug}` : `Session Replay — ${orgSlug}`; - const content = ( + const content = organization.features.includes('replay-details-new-ui') ? ( + + + + + + + + + + + + + + ) : (
@@ -91,6 +106,23 @@ const Header = styled(Layout.Header)` padding-bottom: ${space(1.5)}; @media (min-width: ${p => p.theme.breakpoints.md}) { gap: ${space(1)} ${space(3)}; - padding: ${space(2)} ${space(2)} ${space(1.5)} ${space(2)}; + padding: ${space(2)} ${space(1)} ${space(0.5)} ${space(2)}; } `; + +const NewTopHeader = styled('div')` + padding-left: ${p => p.theme.space.lg}; + padding-right: ${p => p.theme.space.lg}; + border-bottom: 1px solid ${p => p.theme.innerBorder}; + display: flex; + align-items: center; + justify-content: space-between; + gap: ${space(1)}; + flex-flow: row wrap; + height: 44px; +`; + +const NewBottonHeader = styled(Flex)` + padding: ${p => p.theme.space.md} ${p => p.theme.space.lg}; + border-bottom: 1px solid ${p => p.theme.innerBorder}; +`; diff --git a/static/app/views/settings/project/tempest/CredentialRow.tsx b/static/app/views/settings/project/tempest/CredentialRow.tsx index 7895a1b38031a6..4ae4f421611e8e 100644 --- a/static/app/views/settings/project/tempest/CredentialRow.tsx +++ b/static/app/views/settings/project/tempest/CredentialRow.tsx @@ -67,7 +67,7 @@ export function CredentialRow({ } type StatusTagProps = { - statusType: 'error' | 'success' | 'pending'; + statusType: 'error' | 'success' | 'pending' | 'warning'; message?: string; }; @@ -75,6 +75,7 @@ const STATUS_CONFIG = { error: {label: 'Error', type: 'error'}, success: {label: 'Active', type: 'default'}, pending: {label: 'Pending', type: 'info'}, + warning: {label: 'Active', type: 'warning'}, } as const; function StatusTag({statusType, message}: StatusTagProps) { @@ -95,5 +96,13 @@ function getStatusType(credential: { return 'pending'; } - return credential.messageType === MessageType.ERROR ? 'error' : 'success'; + if (credential.messageType === MessageType.ERROR) { + return 'error'; + } + + if (credential.messageType === MessageType.WARNING) { + return 'warning'; + } + + return 'success'; } diff --git a/static/app/views/settings/project/tempest/types.ts b/static/app/views/settings/project/tempest/types.ts index e2521612d18cb7..b17bb233036c02 100644 --- a/static/app/views/settings/project/tempest/types.ts +++ b/static/app/views/settings/project/tempest/types.ts @@ -1,6 +1,8 @@ export enum MessageType { SUCCESS = 'success', ERROR = 'error', + WARNING = 'warning', + INFO = 'info', } export type TempestCredentials = { diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx index 546b31542ba7c7..2c0615becd38b8 100644 --- a/static/app/views/settings/projectSeer/index.spec.tsx +++ b/static/app/views/settings/projectSeer/index.spec.tsx @@ -12,6 +12,7 @@ import { within, } from 'sentry-test/reactTestingLibrary'; +import * as indicators from 'sentry/actionCreators/indicator'; import type {SeerPreferencesResponse} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -1224,4 +1225,385 @@ describe('ProjectSeer', () => { ).not.toBeInTheDocument(); }); }); + + describe('Auto-open PR and Cursor Handoff toggles with triage-signals-v0', () => { + it('shows Auto-open PR toggle when Auto-Trigger is ON', async () => { + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + await screen.findByText(/Automation/i); + expect(screen.getByRole('checkbox', {name: /Auto-open PR/i})).toBeInTheDocument(); + }); + + it('hides Auto-open PR toggle when Auto-Trigger is OFF', async () => { + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'off', + }), + }, + }); + + await screen.findByText(/Automation/i); + expect( + screen.queryByRole('checkbox', {name: /Auto-open PR/i}) + ).not.toBeInTheDocument(); + }); + + it('shows Cursor handoff toggle when Auto-Trigger is ON and Cursor integration exists', async () => { + const orgWithCursor = OrganizationFixture({ + features: ['autofix-seer-preferences', 'integrations-cursor'], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`, + method: 'GET', + body: { + setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true}, + billing: {hasAutofixQuota: true, hasScannerQuota: true}, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/repos/`, + query: {status: 'active'}, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'GET', + body: {code_mapping_repos: []}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`, + method: 'GET', + body: { + integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}], + }, + }); + + render(, { + organization: orgWithCursor, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + await screen.findByText(/Automation/i); + expect( + screen.getByRole('checkbox', {name: /Hand off to Cursor/i}) + ).toBeInTheDocument(); + }); + + it('hides Cursor handoff toggle when no Cursor integration', async () => { + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + await screen.findByText(/Automation/i); + expect( + screen.queryByRole('checkbox', {name: /Hand off to Cursor/i}) + ).not.toBeInTheDocument(); + }); + + it('updates preferences when Auto-open PR toggle is changed', async () => { + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/`, + method: 'PUT', + body: {}, + }); + + const seerPreferencesPostRequest = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`, + method: 'POST', + }); + + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + const toggle = await screen.findByRole('checkbox', {name: /Auto-open PR/i}); + await userEvent.click(toggle); + + await waitFor(() => { + expect(seerPreferencesPostRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + data: expect.objectContaining({ + automated_run_stopping_point: 'open_pr', + automation_handoff: undefined, + }), + }) + ); + }); + }); + + it('updates preferences when Cursor handoff toggle is changed', async () => { + const orgWithCursor = OrganizationFixture({ + features: ['autofix-seer-preferences', 'integrations-cursor'], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`, + method: 'GET', + body: { + setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true}, + billing: {hasAutofixQuota: true, hasScannerQuota: true}, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/repos/`, + query: {status: 'active'}, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'GET', + body: {code_mapping_repos: []}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`, + method: 'GET', + body: { + integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}], + }, + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/`, + method: 'PUT', + body: {}, + }); + + const seerPreferencesPostRequest = MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'POST', + }); + + render(, { + organization: orgWithCursor, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + const toggle = await screen.findByRole('checkbox', {name: /Hand off to Cursor/i}); + await userEvent.click(toggle); + + await waitFor(() => { + expect(seerPreferencesPostRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + data: expect.objectContaining({ + automated_run_stopping_point: 'root_cause', + automation_handoff: { + handoff_point: 'root_cause', + target: 'cursor_background_agent', + integration_id: 123, + auto_create_pr: false, + }, + }), + }) + ); + }); + }); + + it('shows error when Cursor handoff fails due to missing integration', async () => { + const orgWithCursor = OrganizationFixture({ + features: ['autofix-seer-preferences', 'integrations-cursor'], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`, + method: 'GET', + body: { + setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true}, + billing: {hasAutofixQuota: true, hasScannerQuota: true}, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/repos/`, + query: {status: 'active'}, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'GET', + body: {code_mapping_repos: []}, + }); + + // Mock integrations endpoint returning empty array (no Cursor integration) + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`, + method: 'GET', + body: {integrations: []}, + }); + + render(, { + organization: orgWithCursor, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + await screen.findByText(/Automation/i); + + // Toggle should not be visible when no Cursor integration exists + expect( + screen.queryByRole('checkbox', {name: /Hand off to Cursor/i}) + ).not.toBeInTheDocument(); + }); + + it('shows error message when Auto-open PR toggle fails', async () => { + jest.spyOn(indicators, 'addErrorMessage'); + + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/`, + method: 'PUT', + body: {}, + }); + + const seerPreferencesPostRequest = MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`, + method: 'POST', + statusCode: 500, + body: {detail: 'Internal Server Error'}, + }); + + render(, { + organization, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + const toggle = await screen.findByRole('checkbox', {name: /Auto-open PR/i}); + await userEvent.click(toggle); + + await waitFor(() => { + expect(seerPreferencesPostRequest).toHaveBeenCalled(); + }); + + // Should show error message + expect(indicators.addErrorMessage).toHaveBeenCalledWith( + 'Failed to update auto-open PR setting' + ); + }); + + it('shows error message when Cursor handoff toggle fails', async () => { + jest.spyOn(indicators, 'addErrorMessage'); + + const orgWithCursor = OrganizationFixture({ + features: ['autofix-seer-preferences', 'integrations-cursor'], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/seer/setup-check/`, + method: 'GET', + body: { + setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true}, + billing: {hasAutofixQuota: true, hasScannerQuota: true}, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/repos/`, + query: {status: 'active'}, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'GET', + body: {code_mapping_repos: []}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${orgWithCursor.slug}/integrations/coding-agents/`, + method: 'GET', + body: { + integrations: [{id: '123', name: 'Cursor', provider: 'cursor'}], + }, + }); + + MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/`, + method: 'PUT', + body: {}, + }); + + const seerPreferencesPostRequest = MockApiClient.addMockResponse({ + url: `/projects/${orgWithCursor.slug}/${project.slug}/seer/preferences/`, + method: 'POST', + statusCode: 500, + body: {detail: 'Internal Server Error'}, + }); + + render(, { + organization: orgWithCursor, + outletContext: { + project: ProjectFixture({ + features: ['triage-signals-v0'], + autofixAutomationTuning: 'medium', + }), + }, + }); + + const toggle = await screen.findByRole('checkbox', {name: /Hand off to Cursor/i}); + await userEvent.click(toggle); + + await waitFor(() => { + expect(seerPreferencesPostRequest).toHaveBeenCalled(); + }); + + // Should show error message + expect(indicators.addErrorMessage).toHaveBeenCalledWith( + 'Failed to update Cursor handoff setting' + ); + }); + }); }); diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx index be0f3112d413cb..a57b023ef5efcf 100644 --- a/static/app/views/settings/projectSeer/index.tsx +++ b/static/app/views/settings/projectSeer/index.tsx @@ -2,12 +2,16 @@ import {Fragment, useCallback} from 'react'; import styled from '@emotion/styled'; import {useQueryClient} from '@tanstack/react-query'; +import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {hasEveryAccess} from 'sentry/components/acl/access'; import FeatureDisabled from 'sentry/components/acl/featureDisabled'; import {LinkButton} from 'sentry/components/core/button/linkButton'; import {Link} from 'sentry/components/core/link'; import {CursorIntegrationCta} from 'sentry/components/events/autofix/cursorIntegrationCta'; -import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; +import { + makeProjectSeerPreferencesQueryKey, + useProjectSeerPreferences, +} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; import {useUpdateProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useUpdateProjectSeerPreferences'; import type {ProjectSeerPreferences} from 'sentry/components/events/autofix/types'; import {useCodingAgentIntegrations} from 'sentry/components/events/autofix/useAutofix'; @@ -254,6 +258,8 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { [updateProjectSeerPreferences, preference?.repositories, cursorIntegration] ); + // Handler for Cursor's "Auto-Create PR" toggle (from PR #103730) + // Controls whether Cursor agent auto-creates PRs const handleAutoCreatePrChange = useCallback( (value: boolean) => { if (!preference?.automation_handoff) { @@ -271,6 +277,7 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { [preference, updateProjectSeerPreferences] ); + // Handler for changing which integration is used for automation handoff const handleIntegrationChange = useCallback( (integrationId: number) => { if (!preference?.automation_handoff) { @@ -288,6 +295,107 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { [preference, updateProjectSeerPreferences] ); + // Handler for Auto-open PR toggle (triage-signals-v0) + // Controls whether Seer auto-opens PRs + // OFF = stop at code_changes, ON = stop at open_pr + const handleAutoOpenPrChange = useCallback( + (value: boolean) => { + updateProjectSeerPreferences( + { + repositories: preference?.repositories || [], + automated_run_stopping_point: value ? 'open_pr' : 'code_changes', + automation_handoff: undefined, // Clear cursor handoff when using Seer PR + }, + { + onError: () => { + addErrorMessage(t('Failed to update auto-open PR setting')); + // Refetch to reset form state to backend value + queryClient.invalidateQueries({ + queryKey: [ + makeProjectSeerPreferencesQueryKey(organization.slug, project.slug), + ], + }); + }, + } + ); + }, + [ + updateProjectSeerPreferences, + preference?.repositories, + queryClient, + organization.slug, + project.slug, + ] + ); + + // Handler for Cursor handoff toggle (triage-signals-v0) + // When ON: stops at root_cause and hands off to Cursor + // When OFF: defaults to code_changes (user can then enable auto-open PR if desired) + const handleCursorHandoffChange = useCallback( + (value: boolean) => { + if (value) { + if (!cursorIntegration) { + addErrorMessage( + t('Cursor integration not found. Please refresh the page and try again.') + ); + return; + } + updateProjectSeerPreferences( + { + repositories: preference?.repositories || [], + automated_run_stopping_point: 'root_cause', + automation_handoff: { + handoff_point: 'root_cause', + target: 'cursor_background_agent', + integration_id: parseInt(cursorIntegration.id, 10), + auto_create_pr: false, + }, + }, + { + onError: () => { + addErrorMessage(t('Failed to update Cursor handoff setting')); + // Refetch to reset form state to backend value + queryClient.invalidateQueries({ + queryKey: [ + makeProjectSeerPreferencesQueryKey(organization.slug, project.slug), + ], + }); + }, + } + ); + } else { + // When turning OFF, default to code_changes + // User can then manually enable auto-open PR if desired + updateProjectSeerPreferences( + { + repositories: preference?.repositories || [], + automated_run_stopping_point: 'code_changes', + automation_handoff: undefined, + }, + { + onError: () => { + addErrorMessage(t('Failed to update Cursor handoff setting')); + // Refetch to reset form state to backend value + queryClient.invalidateQueries({ + queryKey: [ + makeProjectSeerPreferencesQueryKey(organization.slug, project.slug), + ], + }); + }, + } + ); + } + }, + [ + updateProjectSeerPreferences, + preference?.repositories, + cursorIntegration, + queryClient, + organization.slug, + project.slug, + ] + ); + const automatedRunStoppingPointField = { name: 'automated_run_stopping_point', label: t('Where should Seer stop?'), @@ -352,6 +460,48 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { }, } satisfies FieldObject; + // For triage-signals-v0: Simple toggle for Auto-open PR + // OFF = stop at code_changes, ON = stop at open_pr + const autoOpenPrToggleField = { + name: 'autoOpenPr', + label: t('Auto-open PR'), + help: () => + t( + 'When enabled, Seer will automatically open a pull request after writing code changes.' + ), + type: 'boolean', + saveOnBlur: true, + onChange: handleAutoOpenPrChange, + getData: () => ({}), // Prevent default form submission, onChange handles it + visible: ({model}) => { + const tuningValue = model?.getValue('autofixAutomationTuning'); + return typeof tuningValue === 'boolean' ? tuningValue : tuningValue !== 'off'; + }, + disabled: ({model}) => model?.getValue('cursorHandoff') === true, + } satisfies FieldObject; + + // For triage-signals-v0: Simple toggle for Cursor handoff + // When ON: stops at root_cause and hands off to Cursor + const cursorHandoffToggleField = { + name: 'cursorHandoff', + label: t('Hand off to Cursor'), + help: () => + t( + "When enabled, Seer will identify the root cause and hand off the fix to Cursor's cloud agent." + ), + type: 'boolean', + saveOnBlur: true, + onChange: handleCursorHandoffChange, + getData: () => ({}), // Prevent default form submission, onChange handles it + visible: ({model}) => { + const tuningValue = model?.getValue('autofixAutomationTuning'); + const automationEnabled = + typeof tuningValue === 'boolean' ? tuningValue : tuningValue !== 'off'; + return automationEnabled && hasCursorIntegration; + }, + disabled: ({model}) => model?.getValue('autoOpenPr') === true, + } satisfies FieldObject; + const seerFormGroups: JsonFormObject[] = [ { title: ( @@ -380,7 +530,10 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { isTriageSignalsFeatureOn ? autofixAutomationToggleField : autofixAutomatingTuningField, - automatedRunStoppingPointField, + // Flag ON: show new toggles; Flag OFF: show old dropdown + ...(isTriageSignalsFeatureOn + ? [autoOpenPrToggleField, cursorHandoffToggleField] + : [automatedRunStoppingPointField]), ], }, ]; @@ -409,9 +562,15 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { // Same DB field, different UI: toggle (boolean) vs dropdown (string) // When triage signals flag is on, default to true (ON) autofixAutomationTuning: automationTuning, + // For non-flag mode (dropdown) automated_run_stopping_point: preference?.automation_handoff ? 'cursor_handoff' : (preference?.automated_run_stopping_point ?? 'root_cause'), + // For triage-signals-v0 mode (toggles) - only include when flag is on + ...(isTriageSignalsFeatureOn && { + autoOpenPr: preference?.automated_run_stopping_point === 'open_pr', + cursorHandoff: Boolean(preference?.automation_handoff), + }), }} onSubmitSuccess={handleSubmitSuccess} additionalFieldProps={{organization}} diff --git a/static/gsApp/hooks/useMaxPickableDays.tsx b/static/gsApp/hooks/useMaxPickableDays.tsx index 754a93fdc1a030..d45ae76663b429 100644 --- a/static/gsApp/hooks/useMaxPickableDays.tsx +++ b/static/gsApp/hooks/useMaxPickableDays.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react'; import moment from 'moment-timezone'; +import {MAX_PICKABLE_DAYS} from 'sentry/constants'; import {DataCategory} from 'sentry/types/core'; import {defined} from 'sentry/utils'; import { @@ -16,6 +17,11 @@ import type {Subscription} from 'getsentry/types'; import useSubscription from './useSubscription'; +export function useDefaultMaxPickableDays() { + const subscription = useSubscription(); + return subscription?.planDetails?.retentionDays ?? MAX_PICKABLE_DAYS; +} + export function useMaxPickableDays({ dataCategories, }: UseMaxPickableDaysProps): MaxPickableDaysOptions { diff --git a/static/gsApp/registerHooks.tsx b/static/gsApp/registerHooks.tsx index 104457ebcfbdb1..e918b4c9be22b3 100644 --- a/static/gsApp/registerHooks.tsx +++ b/static/gsApp/registerHooks.tsx @@ -82,7 +82,7 @@ import ReplayOnboardingAlert from './components/replayOnboardingAlert'; import ReplaySettingsAlert from './components/replaySettingsAlert'; import useButtonTracking from './hooks/useButtonTracking'; import useGetMaxRetentionDays from './hooks/useGetMaxRetentionDays'; -import {useMaxPickableDays} from './hooks/useMaxPickableDays'; +import {useDefaultMaxPickableDays, useMaxPickableDays} from './hooks/useMaxPickableDays'; import useRouteActivatedHook from './hooks/useRouteActivatedHook'; const PartnershipAgreement = lazy(() => import('getsentry/views/partnershipAgreement')); @@ -233,6 +233,7 @@ const GETSENTRY_HOOKS: Partial = { 'component:crons-list-page-header': () => CronsBillingBanner, 'react-hook:route-activated': useRouteActivatedHook, 'react-hook:use-button-tracking': useButtonTracking, + 'react-hook:use-default-max-pickable-days': useDefaultMaxPickableDays, 'react-hook:use-max-pickable-days': useMaxPickableDays, 'react-hook:use-get-max-retention-days': useGetMaxRetentionDays, 'react-hook:use-metric-detector-limit': useMetricDetectorLimit, diff --git a/tests/acceptance/test_organization_events_v2.py b/tests/acceptance/test_organization_events_v2.py index a00ab71093f8d0..82353da48dfe98 100644 --- a/tests/acceptance/test_organization_events_v2.py +++ b/tests/acceptance/test_organization_events_v2.py @@ -155,7 +155,7 @@ def build_span_tree( @no_silo_test -class OrganizationEventsV2Test(AcceptanceTestCase, SnubaTestCase): +class OrganizationEventsTest(AcceptanceTestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() self.user = self.create_user("foo@example.com", is_superuser=True) diff --git a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py index 1ebc9a83658062..326f4e08af5159 100644 --- a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py +++ b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py @@ -1,5 +1,33 @@ +from django.test import override_settings + from sentry.testutils.cases import APITestCase +SENTRY_BUILTIN_SOURCES_PLATFORM_TEST = { + "public-source-1": { + "id": "sentry:public-1", + "name": "Public Source 1", + "type": "http", + "url": "https://example.com/symbols/", + }, + "public-source-2": { + "id": "sentry:public-2", + "name": "Public Source 2", + "type": "http", + "url": "https://example.com/symbols2/", + }, + "nintendo": { + "id": "sentry:nintendo", + "name": "Nintendo SDK", + "type": "s3", + "bucket": "nintendo-symbols", + "region": "us-east-1", + "access_key": "test-key", + "secret_key": "test-secret", + "layout": {"type": "native"}, + "platforms": ["nintendo-switch"], + }, +} + class BuiltinSymbolSourcesNoSlugTest(APITestCase): endpoint = "sentry-api-0-builtin-symbol-sources" @@ -39,3 +67,77 @@ def test_with_slug(self) -> None: assert "id" in body[0] assert "name" in body[0] assert "hidden" in body[0] + + +class BuiltinSymbolSourcesPlatformFilteringTest(APITestCase): + endpoint = "sentry-api-0-organization-builtin-symbol-sources" + + def setUp(self) -> None: + super().setUp() + self.organization = self.create_organization(owner=self.user) + self.login_as(user=self.user) + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_nintendo_switch_with_access(self) -> None: + """Nintendo Switch platform should see nintendo source only if org has access""" + # Enable nintendo-switch for this organization + self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) + + resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Nintendo Switch with access should see nintendo + assert "nintendo" in source_keys + # Should also see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_nintendo_switch_without_access(self) -> None: + """Nintendo Switch platform should NOT see nintendo if org lacks access""" + # Organization does not have nintendo-switch enabled (default is empty list) + + resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Should NOT see nintendo without console platform access + assert "nintendo" not in source_keys + # Should still see public sources + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_unity(self) -> None: + """Unity platform should NOT see nintendo source""" + resp = self.get_response(self.organization.slug, qs_params={"platform": "unity"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Unity should see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + # Unity should NOT see nintendo (restricted to nintendo-switch) + assert "nintendo" not in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_no_platform_parameter(self) -> None: + """Without platform parameter, should see public sources but not platform-restricted ones""" + resp = self.get_response(self.organization.slug) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Should see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + # Should NOT see platform-restricted source when no platform is provided + assert "nintendo" not in source_keys diff --git a/tests/sentry/api/endpoints/test_project_rule_actions.py b/tests/sentry/api/endpoints/test_project_rule_actions.py index a8859bede66005..b61c6eb0947031 100644 --- a/tests/sentry/api/endpoints/test_project_rule_actions.py +++ b/tests/sentry/api/endpoints/test_project_rule_actions.py @@ -14,7 +14,11 @@ PagerDutyIssueAlertHandler, PluginIssueAlertHandler, ) +from sentry.notifications.notification_action.issue_alert_registry.handlers.sentry_app_issue_alert_handler import ( + SentryAppIssueAlertHandler, +) from sentry.rules.actions.notify_event import NotifyEventAction +from sentry.sentry_apps.services.app.model import RpcAlertRuleActionResult from sentry.shared_integrations.exceptions import IntegrationFormError from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase @@ -338,3 +342,73 @@ def test_email_action( self.get_success_response(self.organization.slug, self.project.slug, actions=action_data) assert mock_notify.call_count == 1 + + @mock.patch( + "sentry.api.endpoints.project_rule_actions.should_fire_workflow_actions", return_value=True + ) + @mock.patch( + "sentry.rules.actions.sentry_apps.utils.app_service.trigger_sentry_app_action_creators" + ) + @mock.patch( + "sentry.notifications.notification_action.registry.group_type_notification_registry.get", + return_value=IssueAlertRegistryHandler, + ) + @mock.patch( + "sentry.notifications.notification_action.registry.issue_alert_handler_registry.get", + return_value=SentryAppIssueAlertHandler, + ) + def test_sentry_app_action( + self, + mock_get_issue_alert_handler, + mock_get_group_type_handler, + mock_trigger_sentry_app_action_creators: mock.MagicMock, + mock_should_fire_workflow_actions: mock.MagicMock, + ) -> None: + self.create_detector(project=self.project) + schema = { + "type": "alert-rule-action", + "title": "Create Task with App", + "settings": { + "type": "alert-rule-settings", + "uri": "/sentry/alert-rule", + "required_fields": [ + {"type": "list", "name": "asdf", "label": "None"}, + {"type": "text", "name": "fdsa", "label": "label"}, + ], + }, + } + self.create_sentry_app( + organization=self.organization, + name="Test Application", + is_alertable=True, + schema={"elements": [schema]}, + ) + install = self.create_sentry_app_installation( + slug="test-application", organization=self.organization + ) + action_data = [ + { + "id": "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction", + "sentryAppInstallationUuid": install.uuid, + "settings": [ + { + "name": "asdf", + "label": None, + "value": [ + {"id": "1dedabd2-059d-457b-ac17-df39031d4593", "type": "team"} + ], # should stringify this + }, + { + "name": "fdsa", + "label": "label", + "value": "string", + }, + ], + "hasSchemaFormConfig": True, + } + ] + mock_trigger_sentry_app_action_creators.return_value = RpcAlertRuleActionResult( + success=True, message="success" + ) + + self.get_success_response(self.organization.slug, self.project.slug, actions=action_data) diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py new file mode 100644 index 00000000000000..4d486ad46fccdb --- /dev/null +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -0,0 +1,129 @@ +from django.test import override_settings + +from sentry.api.helpers.default_symbol_sources import set_default_symbol_sources +from sentry.testutils.cases import TestCase + +# Mock SENTRY_BUILTIN_SOURCES with a platform-restricted source for testing +SENTRY_BUILTIN_SOURCES_TEST = { + "nintendo": { + "id": "sentry:nintendo", + "name": "Nintendo SDK", + "type": "s3", + "bucket": "nintendo-symbols", + "region": "us-east-1", + "access_key": "test-key", + "secret_key": "test-secret", + "layout": {"type": "native"}, + "platforms": ["nintendo-switch"], + }, +} + + +class SetDefaultSymbolSourcesTest(TestCase): + def setUp(self): + super().setUp() + self.organization = self.create_organization(owner=self.user) + + def test_no_platform(self): + """Projects without a platform should keep their epoch defaults""" + project = self.create_project(organization=self.organization, platform=None) + # Capture epoch defaults before calling set_default_symbol_sources + epoch_defaults = project.get_option("sentry:builtin_symbol_sources") + + set_default_symbol_sources(project, self.organization) + + # Should not change the defaults for projects without a platform + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources == epoch_defaults + + def test_unknown_platform(self): + """Projects with unknown platforms should keep their epoch defaults""" + project = self.create_project(organization=self.organization, platform="unknown-platform") + # Capture epoch defaults before calling set_default_symbol_sources + epoch_defaults = project.get_option("sentry:builtin_symbol_sources") + + set_default_symbol_sources(project, self.organization) + + # Should not change the defaults for projects with unknown platforms + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources == epoch_defaults + + def test_electron_platform(self): + """Electron projects should get the correct default sources""" + project = self.create_project(organization=self.organization, platform="electron") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "ios" in sources + assert "microsoft" in sources + assert "electron" in sources + + def test_unity_platform(self): + """Unity projects should get the correct default sources""" + project = self.create_project(organization=self.organization, platform="unity") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "ios" in sources + assert "microsoft" in sources + assert "android" in sources + assert "nuget" in sources + assert "unity" in sources + assert "nvidia" in sources + assert "ubuntu" in sources + + def test_organization_auto_fetch_from_project(self): + """Function should auto-fetch organization from project if not provided""" + project = self.create_project(organization=self.organization, platform="electron") + # Don't pass organization parameter + set_default_symbol_sources(project) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "electron" in sources + + +class PlatformRestrictedSymbolSourcesTest(TestCase): + """Tests for platform-restricted symbol sources (e.g., console platforms)""" + + def setUp(self): + super().setUp() + self.organization = self.create_organization(owner=self.user) + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_TEST) + def test_nintendo_switch_with_org_access(self): + """Nintendo Switch project should get nintendo source if org has access""" + # Grant org access to nintendo-switch console platform + self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) + + project = self.create_project(organization=self.organization, platform="nintendo-switch") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "nintendo" in sources + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_TEST) + def test_nintendo_switch_without_org_access(self): + """Nintendo Switch project should NOT get nintendo source if org lacks access""" + # Org has no enabled console platforms (default is empty list) + project = self.create_project(organization=self.organization, platform="nintendo-switch") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + # Should be empty since no sources are available (nintendo is restricted) + assert sources == [] + + def test_unity_not_affected_by_console_restrictions(self): + """Unity projects should get sources regardless of console platform access""" + # Org has no enabled console platforms + project = self.create_project(organization=self.organization, platform="unity") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + # Unity sources have no platform restrictions, so they should all be added + assert "unity" in sources + assert "microsoft" in sources diff --git a/tests/sentry/core/endpoints/test_organization_index.py b/tests/sentry/core/endpoints/test_organization_index.py index 4b194d90d33cd6..26c8d52321b488 100644 --- a/tests/sentry/core/endpoints/test_organization_index.py +++ b/tests/sentry/core/endpoints/test_organization_index.py @@ -4,7 +4,6 @@ from typing import Any from unittest.mock import MagicMock, patch -import pytest from django.test import override_settings from django.urls import reverse @@ -184,7 +183,6 @@ def test_with_default_team_true(self) -> None: ) OrganizationMemberTeam.objects.get(organizationmember_id=org_member.id, team_id=team.id) - @pytest.mark.skip("flaky: INFRENG-210") def test_valid_slugs(self) -> None: valid_slugs = ["santry", "downtown-canada", "1234-foo"] for input_slug in valid_slugs: diff --git a/tests/sentry/explore/translation/test_alerts_translation.py b/tests/sentry/explore/translation/test_alerts_translation.py index 7152ac400f03e6..8adfe1f0426862 100644 --- a/tests/sentry/explore/translation/test_alerts_translation.py +++ b/tests/sentry/explore/translation/test_alerts_translation.py @@ -4,6 +4,9 @@ import orjson import pytest from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType +from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ( + ExtrapolationMode as RPCExtrapolationMode, +) from sentry_protos.snuba.v1.trace_item_attribute_pb2 import Function from sentry_protos.snuba.v1.trace_item_filter_pb2 import ComparisonFilter from urllib3.response import HTTPResponse @@ -25,7 +28,7 @@ StoreDataResponse, ) from sentry.snuba.dataset import Dataset -from sentry.snuba.models import SnubaQuery, SnubaQueryEventType +from sentry.snuba.models import ExtrapolationMode, SnubaQuery, SnubaQueryEventType from sentry.snuba.subscriptions import create_snuba_query, create_snuba_subscription from sentry.testutils.cases import SnubaTestCase, TestCase from sentry.testutils.helpers.features import with_feature @@ -160,6 +163,7 @@ def test_translate_alert_rule_simple_count(self, mock_create_rpc) -> None: assert snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value assert snuba_query.aggregate == "count(span.duration)" assert snuba_query.query == "(span.duration:>100) AND is_transaction:1" + assert snuba_query.extrapolation_mode == ExtrapolationMode.SERVER_WEIGHTED.value event_types = list( SnubaQueryEventType.objects.filter(snuba_query=snuba_query).values_list( @@ -202,6 +206,10 @@ def test_translate_alert_rule_simple_count(self, mock_create_rpc) -> None: assert expression.aggregation.key.name == "sentry.project_id" assert expression.aggregation.label == "count(span.duration)" assert expression.label == "count(span.duration)" + assert ( + expression.aggregation.extrapolation_mode + == RPCExtrapolationMode.EXTRAPOLATION_MODE_SERVER_ONLY + ) assert len(rpc_time_series_request.group_by) == 0 @@ -256,6 +264,7 @@ def test_translate_alert_rule_p95(self, mock_create_rpc) -> None: assert snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value assert snuba_query.aggregate == "p95(span.duration)" assert snuba_query.query == "(transaction.method:GET) AND is_transaction:1" + assert snuba_query.extrapolation_mode == ExtrapolationMode.CLIENT_AND_SERVER_WEIGHTED.value event_types = list( SnubaQueryEventType.objects.filter(snuba_query=snuba_query).values_list( @@ -321,6 +330,7 @@ def test_translate_alert_rule_count_unique(self, mock_create_rpc) -> None: assert snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value assert snuba_query.aggregate == "count_unique(user)" assert snuba_query.query == "(transaction:/api/*) AND is_transaction:1" + assert snuba_query.extrapolation_mode == ExtrapolationMode.SERVER_WEIGHTED.value assert mock_create_rpc.called call_args = mock_create_rpc.call_args @@ -377,6 +387,7 @@ def test_translate_alert_rule_empty_query(self, mock_create_rpc) -> None: assert snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value assert snuba_query.aggregate == "count(span.duration)" assert snuba_query.query == "is_transaction:1" + assert snuba_query.extrapolation_mode == ExtrapolationMode.NONE.value assert mock_create_rpc.called call_args = mock_create_rpc.call_args @@ -733,3 +744,243 @@ def test_rollback_anomaly_detection_alert( assert project_arg.id == self.project.id assert seer_method_arg == SeerMethod.UPDATE assert event_types_arg == [SnubaQueryEventType.EventType.TRANSACTION] + + @with_feature("organizations:migrate-transaction-alerts-to-spans") + @patch("sentry.snuba.tasks._create_rpc_in_snuba") + def test_extrapolation_mode_sum_performance_metrics(self, mock_create_rpc) -> None: + mock_create_rpc.return_value = "test-subscription-id" + + snuba_query = create_snuba_query( + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.PerformanceMetrics, + query="", + aggregate="sum(transaction.duration)", + time_window=timedelta(minutes=10), + environment=None, + event_types=[SnubaQueryEventType.EventType.TRANSACTION], + resolution=timedelta(minutes=1), + ) + + create_snuba_subscription( + project=self.project, + subscription_type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + snuba_query=snuba_query, + ) + + data_source = self.create_data_source( + organization=self.org, + source_id=str(snuba_query.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + + detector_data_condition_group = self.create_data_condition_group( + organization=self.org, + ) + + detector = self.create_detector( + name="Test Detector", + type=MetricIssue.slug, + project=self.project, + config={"detection_type": AlertRuleDetectionType.STATIC.value}, + workflow_condition_group=detector_data_condition_group, + ) + + data_source.detectors.add(detector) + + with self.tasks(): + translate_detector_and_update_subscription_in_snuba(snuba_query) + snuba_query.refresh_from_db() + + assert snuba_query.extrapolation_mode == ExtrapolationMode.SERVER_WEIGHTED.value + + @with_feature("organizations:migrate-transaction-alerts-to-spans") + @patch("sentry.snuba.tasks._create_rpc_in_snuba") + def test_extrapolation_mode_sum_transactions(self, mock_create_rpc) -> None: + mock_create_rpc.return_value = "test-subscription-id" + + snuba_query = create_snuba_query( + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.Transactions, + query="", + aggregate="sum(transaction.duration)", + time_window=timedelta(minutes=10), + environment=None, + event_types=[SnubaQueryEventType.EventType.TRANSACTION], + resolution=timedelta(minutes=1), + ) + + create_snuba_subscription( + project=self.project, + subscription_type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + snuba_query=snuba_query, + ) + + data_source = self.create_data_source( + organization=self.org, + source_id=str(snuba_query.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + + detector_data_condition_group = self.create_data_condition_group( + organization=self.org, + ) + + detector = self.create_detector( + name="Test Detector", + type=MetricIssue.slug, + project=self.project, + config={"detection_type": AlertRuleDetectionType.STATIC.value}, + workflow_condition_group=detector_data_condition_group, + ) + + data_source.detectors.add(detector) + + with self.tasks(): + translate_detector_and_update_subscription_in_snuba(snuba_query) + snuba_query.refresh_from_db() + + assert snuba_query.extrapolation_mode == ExtrapolationMode.NONE.value + + @with_feature("organizations:migrate-transaction-alerts-to-spans") + @patch("sentry.snuba.tasks._create_rpc_in_snuba") + def test_extrapolation_mode_count_if_performance_metrics(self, mock_create_rpc) -> None: + mock_create_rpc.return_value = "test-subscription-id" + + snuba_query = create_snuba_query( + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.PerformanceMetrics, + query="", + aggregate="count_if(transaction.duration,greater,100)", + time_window=timedelta(minutes=10), + environment=None, + event_types=[SnubaQueryEventType.EventType.TRANSACTION], + resolution=timedelta(minutes=1), + ) + + create_snuba_subscription( + project=self.project, + subscription_type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + snuba_query=snuba_query, + ) + + data_source = self.create_data_source( + organization=self.org, + source_id=str(snuba_query.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + + detector_data_condition_group = self.create_data_condition_group( + organization=self.org, + ) + + detector = self.create_detector( + name="Test Detector", + type=MetricIssue.slug, + project=self.project, + config={"detection_type": AlertRuleDetectionType.STATIC.value}, + workflow_condition_group=detector_data_condition_group, + ) + + data_source.detectors.add(detector) + + with self.tasks(): + translate_detector_and_update_subscription_in_snuba(snuba_query) + snuba_query.refresh_from_db() + + assert snuba_query.extrapolation_mode == ExtrapolationMode.SERVER_WEIGHTED.value + + @with_feature("organizations:migrate-transaction-alerts-to-spans") + @patch("sentry.snuba.tasks._create_rpc_in_snuba") + def test_extrapolation_mode_count_if_transactions(self, mock_create_rpc) -> None: + mock_create_rpc.return_value = "test-subscription-id" + + snuba_query = create_snuba_query( + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.Transactions, + query="", + aggregate="count_if(transaction.duration,greater,100)", + time_window=timedelta(minutes=10), + environment=None, + event_types=[SnubaQueryEventType.EventType.TRANSACTION], + resolution=timedelta(minutes=1), + ) + + create_snuba_subscription( + project=self.project, + subscription_type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + snuba_query=snuba_query, + ) + + data_source = self.create_data_source( + organization=self.org, + source_id=str(snuba_query.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + + detector_data_condition_group = self.create_data_condition_group( + organization=self.org, + ) + + detector = self.create_detector( + name="Test Detector", + type=MetricIssue.slug, + project=self.project, + config={"detection_type": AlertRuleDetectionType.STATIC.value}, + workflow_condition_group=detector_data_condition_group, + ) + + data_source.detectors.add(detector) + + with self.tasks(): + translate_detector_and_update_subscription_in_snuba(snuba_query) + snuba_query.refresh_from_db() + + assert snuba_query.extrapolation_mode == ExtrapolationMode.NONE.value + + @with_feature("organizations:migrate-transaction-alerts-to-spans") + @patch("sentry.snuba.tasks._create_rpc_in_snuba") + def test_extrapolation_mode_p50_transactions(self, mock_create_rpc) -> None: + mock_create_rpc.return_value = "test-subscription-id" + + snuba_query = create_snuba_query( + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.Transactions, + query="", + aggregate="p50(transaction.duration)", + time_window=timedelta(minutes=10), + environment=None, + event_types=[SnubaQueryEventType.EventType.TRANSACTION], + resolution=timedelta(minutes=1), + ) + + create_snuba_subscription( + project=self.project, + subscription_type=INCIDENTS_SNUBA_SUBSCRIPTION_TYPE, + snuba_query=snuba_query, + ) + + data_source = self.create_data_source( + organization=self.org, + source_id=str(snuba_query.id), + type=DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION, + ) + + detector_data_condition_group = self.create_data_condition_group( + organization=self.org, + ) + + detector = self.create_detector( + name="Test Detector", + type=MetricIssue.slug, + project=self.project, + config={"detection_type": AlertRuleDetectionType.STATIC.value}, + workflow_condition_group=detector_data_condition_group, + ) + + data_source.detectors.add(detector) + + with self.tasks(): + translate_detector_and_update_subscription_in_snuba(snuba_query) + snuba_query.refresh_from_db() + + assert snuba_query.extrapolation_mode == ExtrapolationMode.CLIENT_AND_SERVER_WEIGHTED.value diff --git a/tests/sentry/incidents/endpoints/serializers/test_alert_rule.py b/tests/sentry/incidents/endpoints/serializers/test_alert_rule.py index 54f0285fe90190..e0fec74e1c9d6c 100644 --- a/tests/sentry/incidents/endpoints/serializers/test_alert_rule.py +++ b/tests/sentry/incidents/endpoints/serializers/test_alert_rule.py @@ -291,6 +291,31 @@ def test_combined_serializer(self) -> None: serialized_uptime_monitor["type"] = "uptime" assert result[3] == serialized_uptime_monitor + @patch("sentry.api.serializers.models.rule.RuleSerializer.serialize") + @patch("sentry.api.serializers.base.logger") + def test_combined_serializer_failure(self, mock_logger, mock_serialize: MagicMock) -> None: + mock_serialize.side_effect = Exception + + projects = [self.project, self.create_project()] + alert_rule = self.create_alert_rule(projects=projects) + issue_rule = self.create_issue_alert_rule( + data={ + "project": self.project, + "name": "Issue Rule Test", + "conditions": [], + "actions": [], + "actionMatch": "all", + } + ) + result = serialize( + [alert_rule, issue_rule], + serializer=CombinedRuleSerializer(), + ) + assert mock_logger.exception.call_count == 1 + self.assert_alert_rule_serialized(alert_rule, result[0]) + # we have limited data here because of the exception + assert result[1] == {"type": "rule"} + def test_alert_snoozed(self) -> None: projects = [self.project, self.create_project()] alert_rule = self.create_alert_rule(projects=projects) diff --git a/tests/sentry/integrations/slack/test_unfurl.py b/tests/sentry/integrations/slack/test_unfurl.py index 279cac69772026..5b12de290093dd 100644 --- a/tests/sentry/integrations/slack/test_unfurl.py +++ b/tests/sentry/integrations/slack/test_unfurl.py @@ -484,7 +484,7 @@ def test_unfurl_metric_alerts_chart_eap_spans(self, mock_generate_chart: MagicMo assert chart_data["incidents"][0]["id"] == str(incident.id) @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", ) @patch("sentry.charts.backend.generate_chart", return_value="chart-url") def test_unfurl_metric_alerts_chart_eap_spans_events_stats_call( @@ -531,7 +531,7 @@ def test_unfurl_metric_alerts_chart_eap_spans_events_stats_call( assert dataset == Spans @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", ) @patch("sentry.charts.backend.generate_chart", return_value="chart-url") def test_unfurl_metric_alerts_chart_eap_ourlogs_events_stats_call( @@ -627,7 +627,7 @@ def test_unfurl_metric_alerts_chart_crash_free(self, mock_generate_chart: MagicM assert len(chart_data["incidents"]) == 0 @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, @@ -662,7 +662,7 @@ def test_unfurl_discover(self, mock_generate_chart: MagicMock, _: MagicMock) -> assert len(chart_data["stats"]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [ (i * INTERVAL_COUNT, [{"count": 0}]) for i in range(int(INTERVALS_PER_DAY / 6)) @@ -702,7 +702,7 @@ def test_unfurl_discover_previous_period( assert len(chart_data["stats"]["data"]) == 48 @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "count()": { "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], @@ -750,7 +750,7 @@ def test_unfurl_discover_multi_y_axis( assert len(chart_data["stats"]["count_unique(user)"]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, @@ -787,7 +787,7 @@ def test_unfurl_discover_html_escaped( assert len(chart_data["stats"]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "default,first,capable-hagfish,None": { "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], @@ -859,7 +859,7 @@ def test_unfurl_discover_short_url(self, mock_generate_chart: MagicMock, _: Magi assert len(chart_data["stats"][first_key]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, @@ -921,7 +921,7 @@ def test_unfurl_correct_y_axis_for_saved_query( assert len(chart_data["stats"]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "default,first": { "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], @@ -976,7 +976,7 @@ def test_top_events_url_param(self, mock_generate_chart: MagicMock, _: MagicMock # patched return value determined by reading events stats output @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "default,second": { "data": [(1212121, [{"count": 15}]), (1652659200, [{"count": 12}])], @@ -1044,7 +1044,7 @@ def test_top_daily_events_renders_bar_chart( assert len(chart_data["stats"][first_key]["data"]) == 2 @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, @@ -1102,7 +1102,7 @@ def test_unfurl_discover_short_url_without_project_ids( assert len(chart_data["stats"]["data"]) == INTERVALS_PER_DAY @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, @@ -1149,7 +1149,7 @@ def test_unfurl_discover_without_project_ids( # patched return value determined by reading events stats output @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "default,second": { "data": [(1212121, [{"count": 15}]), (1652659200, [{"count": 12}])], @@ -1328,7 +1328,7 @@ def test_saved_query_with_interval( assert api_mock.call_args[1]["params"]["interval"] == "10m" @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", ) @patch("sentry.charts.backend.generate_chart", return_value="chart-url") def test_saved_query_with_dataset( @@ -1383,7 +1383,7 @@ def test_saved_query_with_dataset( assert dataset == transactions @patch( - "sentry.api.bases.organization_events.OrganizationEventsV2EndpointBase.get_event_stats_data", + "sentry.api.bases.organization_events.OrganizationEventsEndpointBase.get_event_stats_data", return_value={ "data": [(i * INTERVAL_COUNT, [{"count": 0}]) for i in range(INTERVALS_PER_DAY)], "end": 1652903400, diff --git a/tests/sentry/issues/endpoints/test_organization_group_search_views_starred.py b/tests/sentry/issues/endpoints/test_organization_group_search_views_starred.py index bf9dd4d97e189e..4247a766d9ceb0 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_search_views_starred.py +++ b/tests/sentry/issues/endpoints/test_organization_group_search_views_starred.py @@ -3,7 +3,9 @@ from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewVisibility from sentry.models.groupsearchviewlastvisited import GroupSearchViewLastVisited from sentry.models.groupsearchviewstarred import GroupSearchViewStarred +from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase +from sentry.testutils.silo import assume_test_silo_mode from sentry.users.models.user import User @@ -109,3 +111,40 @@ def test_views_starred_by_many_users(self) -> None: assert len(response.data) == 1 assert response.data[0]["id"] == str(u1_view_1.id) + + def test_handles_none_from_user_service(self) -> None: + """ + Test that when user_service.serialize_many() returns None for a user, + the endpoint handles it gracefully without crashing. + + This can happen when a user is deleted from the system but their views remain. + + Ref: https://linear.app/getsentry/issue/ISWF-719 + """ + self.login_as(user=self.user) + + # Create a second user and member + deleted_user = self.create_user() + self.create_member(user=deleted_user, organization=self.organization) + + # Create views by both users + active_user_view = self.create_view(user=self.user, name="Active User View") + deleted_user_view = self.create_view(user=deleted_user, name="Deleted User View") + + # Star both views as self.user + self.star_view(user=self.user, view=active_user_view) + self.star_view(user=self.user, view=deleted_user_view) + + # Delete the user who created one of the views + with assume_test_silo_mode(SiloMode.CONTROL): + deleted_user.delete() + + response = self.get_success_response(self.organization.slug) + + # Both views should be returned without crashing + assert len(response.data) == 2 + + # One view should have createdBy populated, the other should be None + created_by_values = [view.get("createdBy") for view in response.data] + assert any(cb is not None for cb in created_by_values) + assert any(cb is None for cb in created_by_values) diff --git a/tests/sentry/issues/endpoints/test_organization_issue_timeseries.py b/tests/sentry/issues/endpoints/test_organization_issue_timeseries.py index 6b9b38cc7cb4e7..1fe8a928a8ac3d 100644 --- a/tests/sentry/issues/endpoints/test_organization_issue_timeseries.py +++ b/tests/sentry/issues/endpoints/test_organization_issue_timeseries.py @@ -523,3 +523,47 @@ def test_other_with_resolved_issues(self) -> None: "valueUnit": None, "interval": 3_600_000, } + + def test_buckets_not_filling(self) -> None: + self.start = (self.end - timedelta(days=14)).replace(microsecond=1234) + + project = self.create_project() + self.create_group( + project=project, + status=1, + first_seen=self.start + timedelta(days=1), + resolved_at=self.start + timedelta(days=1, hours=1), + type=2, + ) + response = self.do_request( + { + "start": self.start, + "end": self.end, + "project": project.id, + "interval": "12h", + "category": "issue", + "yAxis": "count(new_issues)", + }, + ) + assert response.status_code == 200, response.content + assert response.data["meta"] == { + "dataset": "issue", + "start": self.start.timestamp() * 1000, + "end": self.end.timestamp() * 1000, + } + assert len(response.data["timeSeries"]) == 1 + timeseries = response.data["timeSeries"][0] + assert len(timeseries["values"]) == 29 + assert timeseries["values"] == [ + { + "incomplete": False, + "timestamp": round((self.start + timedelta(hours=x * 12)).timestamp() * 1000) - 1, + "value": 1 if x == 2 else 0, + } + for x in range(29) + ] + assert timeseries["meta"] == { + "valueType": "integer", + "valueUnit": None, + "interval": 43_200_000, + } diff --git a/tests/sentry/notifications/notification_action/test_issue_alert_registry_handlers.py b/tests/sentry/notifications/notification_action/test_issue_alert_registry_handlers.py index 516791ebdb9775..64db25e19efabb 100644 --- a/tests/sentry/notifications/notification_action/test_issue_alert_registry_handlers.py +++ b/tests/sentry/notifications/notification_action/test_issue_alert_registry_handlers.py @@ -606,26 +606,6 @@ def test_build_rule_action_blob(self) -> None: blob = self.handler.build_rule_action_blob(action, self.organization.id) assert blob == healed - def test_build_rule_action_blob_fallthrough_type_camel_case(self) -> None: - """ - Test that while we are temporarily allowing both fallthroughType and fallthrough_type in Action.data - that both work when building the action data blob and result in the snake case version - """ - action = Action.objects.create( - type=Action.Type.EMAIL, - data={"fallthroughType": FallthroughChoiceType.ACTIVE_MEMBERS}, - config={ - "target_type": ActionTarget.ISSUE_OWNERS, - "target_identifier": None, - }, - ) - blob = self.handler.build_rule_action_blob(action, self.organization.id) - assert blob == { - "fallthrough_type": FallthroughChoiceType.ACTIVE_MEMBERS, - "id": "sentry.mail.actions.NotifyEmailAction", - "targetType": "IssueOwners", - } - class TestPluginIssueAlertHandler(BaseWorkflowTest): def setUp(self) -> None: diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 015e0fa03abeb8..3ccc668c6b3af3 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -771,7 +771,7 @@ def test_stopping_point_mapping(self, score, expected): assert _get_stopping_point_from_fixability(score) == expected -@with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) +@with_feature({"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True}) class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -851,7 +851,7 @@ def test_without_feature_flag(self, mock_gen, mock_budget, mock_state, mock_rate ) with self.feature( - {"organizations:gen-ai-features": True, "projects:triage-signals-v0": False} + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": False} ): run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT) @@ -974,7 +974,7 @@ def test_upper_bound_combinations(self, fixability, user_pref, expected): assert result == expected -@with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) +@with_feature({"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True}) class TestRunAutomationWithUpperBound(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1080,7 +1080,7 @@ def test_no_user_preference_uses_fixability_only( @with_feature("organizations:gen-ai-features") -@with_feature("projects:triage-signals-v0") +@with_feature("organizations:triage-signals-v0-org") class TestRunAutomationAlertEventCount(APITestCase, SnubaTestCase): def setUp(self) -> None: super().setUp() @@ -1096,7 +1096,7 @@ def setUp(self) -> None: def test_alert_skips_automation_below_threshold( self, mock_budget, mock_state, mock_fixability, mock_trigger ): - """Alert automation should skip when event count < 10 with triage-signals-v0""" + """Alert automation should skip when event count < 10 with triage-signals-v0-org""" self.project.update_option("sentry:autofix_automation_tuning", "always") mock_budget.return_value = True mock_state.return_value = None diff --git a/tests/sentry/seer/explorer/test_tools.py b/tests/sentry/seer/explorer/test_tools.py index 6d54e7ec164db7..4975d1a068a90b 100644 --- a/tests/sentry/seer/explorer/test_tools.py +++ b/tests/sentry/seer/explorer/test_tools.py @@ -1915,18 +1915,17 @@ def test_get_log_attributes_for_trace_basic(self) -> None: ts = datetime.fromisoformat(auth_log["timestamp"]).timestamp() assert int(ts) == auth_log_expected.timestamp.seconds - for name, value, type in [ - ("message", "User authentication failed", "str"), - ("project", self.project.slug, "str"), - ("project.id", self.project.id, "int"), - ("severity", "ERROR", "str"), - ("my-string-attribute", "custom value", "str"), - ("my-boolean-attribute", True, "double"), - ("my-double-attribute", 1.23, "double"), - ("my-integer-attribute", 123, "double"), + for name, value in [ + ("message", "User authentication failed"), + ("project", self.project.slug), + ("project.id", self.project.id), + ("severity", "ERROR"), + ("my-string-attribute", "custom value"), + ("my-boolean-attribute", True), + ("my-double-attribute", 1.23), + ("my-integer-attribute", 123), ]: assert auth_log["attributes"][name]["value"] == value, name - assert auth_log["attributes"][name]["type"] == type, f"{name} type mismatch" def test_get_log_attributes_for_trace_substring_filter(self) -> None: result = get_log_attributes_for_trace( @@ -2066,21 +2065,20 @@ def test_get_metric_attributes_for_trace_basic(self) -> None: ts = datetime.fromisoformat(http_metric["timestamp"]).timestamp() assert int(ts) == http_metric_expected.timestamp.seconds - for name, value, type in [ - ("metric.name", "http.request.duration", "str"), - ("metric.type", "distribution", "str"), - ("value", 125.5, "double"), - ("project", self.project.slug, "str"), - ("project.id", self.project.id, "int"), - ("http.method", "GET", "str"), - ("http.status_code", 200, "double"), - ("my-string-attribute", "custom value", "str"), - ("my-boolean-attribute", True, "double"), - ("my-double-attribute", 1.23, "double"), - ("my-integer-attribute", 123, "double"), + for name, value in [ + ("metric.name", "http.request.duration"), + ("metric.type", "distribution"), + ("value", 125.5), + ("project", self.project.slug), + ("project.id", self.project.id), + ("http.method", "GET"), + ("http.status_code", 200), + ("my-string-attribute", "custom value"), + ("my-boolean-attribute", True), + ("my-double-attribute", 1.23), + ("my-integer-attribute", 123), ]: assert http_metric["attributes"][name]["value"] == value, name - assert http_metric["attributes"][name]["type"] == type, f"{name} type mismatch" def test_get_metric_attributes_for_trace_name_filter(self) -> None: # Test substring match (fails) diff --git a/tests/sentry/tasks/test_auto_ongoing_issues.py b/tests/sentry/tasks/test_auto_ongoing_issues.py index 63212e7bcca25e..f18db99669885c 100644 --- a/tests/sentry/tasks/test_auto_ongoing_issues.py +++ b/tests/sentry/tasks/test_auto_ongoing_issues.py @@ -345,6 +345,73 @@ def test_not_all_groups_get_updated(self, mock_metrics_incr) -> None: tags={"count": 0}, ) + @freeze_time("2023-07-12 18:40:00Z") + def test_only_checks_most_recent_regressed_history(self) -> None: + """ + Test that only the MOST RECENT regressed history is checked against the threshold, + not just any regressed history. + + Scenario: + - Group regressed 14 days ago (older than 7-day threshold) + - Group resolved 10 days ago + - Group regressed again 2 days ago (newer than 7-day threshold) + + Expected: Group should NOT be transitioned because most recent regression is only 2 days old + """ + now = datetime.now(tz=timezone.utc) + project = self.create_project() + group = self.create_group( + project=project, + status=GroupStatus.UNRESOLVED, + substatus=GroupSubStatus.REGRESSED, + first_seen=now - timedelta(days=14), + ) + + # Create OLD regressed history (14 days ago) - this is OLDER than threshold + old_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + old_regressed_history.date_added = now - timedelta(days=14) + old_regressed_history.save(update_fields=["date_added"]) + + # Create resolved history in between (10 days ago) + resolved_history = record_group_history( + group, GroupHistoryStatus.RESOLVED, actor=None, release=None + ) + resolved_history.date_added = now - timedelta(days=10) + resolved_history.save(update_fields=["date_added"]) + + # Create NEW regressed history (2 days ago) - this is NEWER than threshold + # This is the MOST RECENT regressed history + new_regressed_history = record_group_history( + group, GroupHistoryStatus.REGRESSED, actor=None, release=None + ) + new_regressed_history.date_added = now - timedelta(days=2) + new_regressed_history.save(update_fields=["date_added"]) + + # Also create a recent group inbox entry + group_inbox = add_group_to_inbox(group, GroupInboxReason.REGRESSION) + group_inbox.date_added = now - timedelta(days=2) + group_inbox.save(update_fields=["date_added"]) + + with self.tasks(): + schedule_auto_transition_to_ongoing() + + # Group should NOT be transitioned because most recent regression is only 2 days old + group.refresh_from_db() + assert group.status == GroupStatus.UNRESOLVED + assert group.substatus == GroupSubStatus.REGRESSED # Should still be REGRESSED + + # Should NOT have created an auto-ongoing activity + assert not Activity.objects.filter( + group=group, type=ActivityType.AUTO_SET_ONGOING.value + ).exists() + + # Should NOT have created an ONGOING history entry + assert not GroupHistory.objects.filter( + group=group, status=GroupHistoryStatus.ONGOING + ).exists() + class ScheduleAutoEscalatingOngoingIssuesTest(TestCase): @freeze_time("2023-07-12 18:40:00Z") diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 88a9a0e1d73197..4c1dd311d89e74 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3033,14 +3033,16 @@ def test_kick_off_seer_automation_with_hide_ai_features_enabled( class TriageSignalsV0TestMixin(BasePostProgressGroupMixin): - """Tests for the triage signals V0 flow behind the projects:triage-signals-v0 feature flag.""" + """Tests for the triage signals V0 flow behind the organizations:triage-signals-v0-org feature flag.""" @patch( "sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner", return_value=True, ) @patch("sentry.tasks.autofix.generate_issue_summary_only.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_less_than_10_no_cache( self, mock_generate_summary_only, mock_get_seer_org_acknowledgement ): @@ -3072,7 +3074,9 @@ def test_triage_signals_event_count_less_than_10_no_cache( return_value=True, ) @patch("sentry.tasks.autofix.generate_issue_summary_only.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_less_than_10_with_cache( self, mock_generate_summary_only, mock_get_seer_org_acknowledgement ): @@ -3105,7 +3109,9 @@ def test_triage_signals_event_count_less_than_10_with_cache( return_value=True, ) @patch("sentry.tasks.autofix.run_automation_only_task.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_gte_10_with_cache( self, mock_run_automation, mock_get_seer_org_acknowledgement ): @@ -3152,7 +3158,9 @@ def mock_buffer_get(model, columns, filters): return_value=True, ) @patch("sentry.tasks.autofix.generate_summary_and_run_automation.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_gte_10_no_cache( self, mock_generate_summary_and_run_automation, mock_get_seer_org_acknowledgement ): @@ -3193,7 +3201,9 @@ def mock_buffer_get(model, columns, filters): return_value=True, ) @patch("sentry.tasks.autofix.run_automation_only_task.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_gte_10_skips_with_seer_last_triggered( self, mock_run_automation, mock_get_seer_org_acknowledgement ): @@ -3241,7 +3251,9 @@ def mock_buffer_get(model, columns, filters): return_value=True, ) @patch("sentry.tasks.autofix.run_automation_only_task.delay") - @with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True}) + @with_feature( + {"organizations:gen-ai-features": True, "organizations:triage-signals-v0-org": True} + ) def test_triage_signals_event_count_gte_10_skips_with_existing_fixability_score( self, mock_run_automation, mock_get_seer_org_acknowledgement ): diff --git a/tests/sentry/tempest/test_tempest.py b/tests/sentry/tempest/test_tempest.py index 79a2a3a33402e6..7af67b88b9f3f0 100644 --- a/tests/sentry/tempest/test_tempest.py +++ b/tests/sentry/tempest/test_tempest.py @@ -33,8 +33,11 @@ def test_fetch_latest_item_id_task_no_id(self, mock_fetch: MagicMock) -> None: fetch_latest_item_id(self.credentials.id) self.credentials.refresh_from_db() - assert self.credentials.message == "No crashes found" - assert self.credentials.message_type == MessageType.ERROR + assert ( + self.credentials.message + == "Connection successful. No crashes found in the crash report system yet. New crashes will appear here automatically when they occur." + ) + assert self.credentials.message_type == MessageType.WARNING assert self.credentials.latest_fetched_item_id is None @patch("sentry.tempest.tasks.fetch_latest_id_from_tempest") diff --git a/tests/sentry/utils/test_snowflake.py b/tests/sentry/utils/test_snowflake.py index 0560948f227f35..8e5abf89244581 100644 --- a/tests/sentry/utils/test_snowflake.py +++ b/tests/sentry/utils/test_snowflake.py @@ -20,6 +20,7 @@ SnowflakeBitSegment, generate_snowflake_id, get_redis_cluster, + get_timestamp_redis_key, uses_snowflake_id, ) @@ -64,14 +65,16 @@ def test_generate_correct_ids_with_region_sequence(self) -> None: @freeze_time(CURRENT_TIME) def test_out_of_region_sequences(self) -> None: - cluster = get_redis_cluster("test_redis_key") + cluster = get_redis_cluster() current_timestamp = int(datetime.now().timestamp() - settings.SENTRY_SNOWFLAKE_EPOCH_START) + redis_key = "test_redis_key" + for i in range(int(_TTL.total_seconds())): timestamp = current_timestamp - i - cluster.set(str(timestamp), 16) + cluster.set(get_timestamp_redis_key(redis_key, timestamp), 16) with pytest.raises(Exception) as context: - generate_snowflake_id("test_redis_key") + generate_snowflake_id(redis_key) assert str(context.value) == "No available ID" diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_incident_groupopenperiod.py b/tests/sentry/workflow_engine/endpoints/test_organization_incident_groupopenperiod.py index 54729e5f00552a..cdf5125ba2a9d7 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_incident_groupopenperiod.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_incident_groupopenperiod.py @@ -1,4 +1,5 @@ from sentry.api.serializers import serialize +from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id from sentry.incidents.grouptype import MetricIssue from sentry.models.groupopenperiod import GroupOpenPeriod from sentry.testutils.cases import APITestCase @@ -103,3 +104,36 @@ def test_get_with_nonexistent_open_period_id(self) -> None: def test_no_filter_provided(self) -> None: self.get_error_response(self.organization.slug, status_code=400) + + def test_fallback_with_fake_incident_identifier(self) -> None: + """ + Test that when an IGOP doesn't exist, the endpoint falls back to looking up + the GroupOpenPeriod by subtracting 10^9 from the incident_identifier. + This is the reverse of the GOP -> Incident serialization logic. + """ + # Create a group with open period but NO IGOP + group_no_igop = self.create_group(type=MetricIssue.type_id) + open_period_no_igop = GroupOpenPeriod.objects.get(group=group_no_igop) + + # Calculate the fake incident_identifier (same as serializer does) + fake_incident_identifier = get_fake_id_from_object_id(open_period_no_igop.id) + + # Query using the fake incident_identifier + response = self.get_success_response( + self.organization.slug, incident_identifier=str(fake_incident_identifier) + ) + + # Should return a fake IGOP response + assert response.data == { + "incidentId": str(fake_incident_identifier), + "incidentIdentifier": str(fake_incident_identifier), + "groupId": str(group_no_igop.id), + "openPeriodId": str(open_period_no_igop.id), + } + + def test_fallback_with_nonexistent_open_period(self) -> None: + # Use a fake incident_identifier that won't map to any real open period + nonexistent_fake_id = get_fake_id_from_object_id(999999) + self.get_error_response( + self.organization.slug, incident_identifier=str(nonexistent_fake_id), status_code=404 + ) diff --git a/tests/sentry/workflow_engine/migrations/test_0104_action_data_fallthrough_type.py b/tests/sentry/workflow_engine/migrations/test_0104_action_data_fallthrough_type.py index e2b59a01290ee4..d7a5f5c6198e7c 100644 --- a/tests/sentry/workflow_engine/migrations/test_0104_action_data_fallthrough_type.py +++ b/tests/sentry/workflow_engine/migrations/test_0104_action_data_fallthrough_type.py @@ -1,9 +1,12 @@ +import pytest + from sentry.notifications.models.notificationaction import ActionTarget from sentry.notifications.types import FallthroughChoiceType from sentry.testutils.cases import TestMigrations from sentry.workflow_engine.models import Action +@pytest.mark.skip class TestActionDataFallthroughType(TestMigrations): migrate_from = "0103_add_unique_constraint" migrate_to = "0104_action_data_fallthrough_type" diff --git a/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py b/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py index 56d7a83405b223..2de4850479c90b 100644 --- a/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py +++ b/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py @@ -314,9 +314,16 @@ def test_list_metrics(self): # this query does not filter on any metrics, so scan all metrics response = self.do_request( { - "field": ["metric.name", "metric.type", "metric.unit", "count(metric.name)"], + "field": [ + "metric.name", + "metric.type", + "metric.unit", + "count(metric.name)", + "per_second(metric.name)", + ], "orderby": "metric.name", "dataset": self.dataset, + "statsPeriod": "10m", } ) assert response.status_code == 200, response.content @@ -326,24 +333,28 @@ def test_list_metrics(self): "metric.type": "gauge", "metric.unit": None, "count(metric.name)": 2, + "per_second(metric.name)": pytest.approx(2 / 600, abs=0.001), }, { "metric.name": "baz", "metric.type": "distribution", "metric.unit": None, "count(metric.name)": 3, + "per_second(metric.name)": pytest.approx(3 / 600, abs=0.001), }, { "metric.name": "foo", "metric.type": "counter", "metric.unit": None, "count(metric.name)": 1, + "per_second(metric.name)": pytest.approx(1 / 600, abs=0.001), }, { "metric.name": "qux", "metric.type": "distribution", "metric.unit": "millisecond", "count(metric.name)": 4, + "per_second(metric.name)": pytest.approx(4 / 600, abs=0.001), }, ] @@ -412,11 +423,47 @@ def test_aggregation_multiple_embedded_same_metric_name(self): ] def test_aggregation_multiple_embedded_different_metric_name(self): + trace_metrics = [ + self.create_trace_metric("foo", 1, "counter"), + self.create_trace_metric("foo", 2, "counter"), + self.create_trace_metric("bar", 4, "counter"), + self.create_trace_metric("baz", 8, "gauge"), + ] + self.store_trace_metrics(trace_metrics) + response = self.do_request( { "field": [ "count(value,foo,counter,-)", "count(value,bar,counter,-)", + "count(value,baz,gauge,-)", + "per_second(value,foo,counter,-)", + "per_second(value,bar,counter,-)", + "per_second(value,baz,gauge,-)", + ], + "dataset": self.dataset, + "project": self.project.id, + "statsPeriod": "10m", + } + ) + assert response.status_code == 200, response.content + assert response.data["data"] == [ + { + "count(value,foo,counter,-)": 2, + "count(value,bar,counter,-)": 1, + "count(value,baz,gauge,-)": 1, + "per_second(value,foo,counter,-)": pytest.approx(3 / 600, abs=0.001), + "per_second(value,bar,counter,-)": pytest.approx(4 / 600, abs=0.001), + "per_second(value,baz,gauge,-)": pytest.approx(1 / 600, abs=0.001), + }, + ] + + def test_mixing_all_metrics_and_one_metric(self): + response = self.do_request( + { + "field": [ + "count(value,foo,counter,-)", + "per_second(value)", ], "dataset": self.dataset, "project": self.project.id, @@ -425,6 +472,7 @@ def test_aggregation_multiple_embedded_different_metric_name(self): assert response.status_code == 400, response.content assert response.data == { "detail": ErrorDetail( - "Cannot aggregate multiple metrics in 1 query.", code="parse_error" + "Cannot aggregate all metrics and singlular metrics in the same query.", + code="parse_error", ) }