Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1a4fdd4
fix(navigation): show pending invite dot on the organization row
a-lider Sep 30, 2026
be10012
fix(navigation): explain the pending invite dot on hover
a-lider Sep 30, 2026
0b3058b
fix(navigation): polish the pending invite dot and organization row
a-lider Oct 1, 2026
ff9f2d1
Merge remote-tracking branch 'origin/master' into posthog/pending-inv…
a-lider Oct 1, 2026
0fc3d0e
fix(navigation): keep pending invites reachable on project pending de…
a-lider Oct 1, 2026
34311d1
chore(visual): update storybook baselines
posthog[bot] Oct 1, 2026
02bcd3c
chore(today): pin the clock in the first-open briefing test
aspicer Oct 2, 2026
4415adb
fix(billing): default usage and spend to complete days
pawel-cebula Sep 28, 2026
f5dd0d3
fix(billing): restore all-date guidance in tool schemas
pawel-cebula Sep 28, 2026
fb3261b
chore(tasks): bump sandbox @posthog/agent to 2.4.253
scheduled-actions-posthog[bot] Oct 2, 2026
5b5a95b
test(today): cover a call without a timezone on a different briefing day
aspicer Oct 2, 2026
d4a8664
chore(billing): regenerate export date guidance after rebase
pawel-cebula Oct 2, 2026
70ebb1b
Merging 5b5a95b3b43d4dd4a59b999a0408d72e25008e1d into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
b3a4c03
Merging 34311d142baf574566dc8d0534421d79168c882d into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
8d7473f
Merging b3a4c03d264aa17bdcfb7a4a321af60403ae7005 into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
d1830d4
Merging 70ebb1b4778f4e1a7bfe5ec3c375d3eaa4160298 into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
6cb0538
Merging fb3261b472eb34c8f03c918c88107ac2d1833d5b into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
2d48d1f
Merging d4a8664d7001b74acadb557b5b11eb7e023ee0eb into trunk-temp/pr-1…
trunk-io[bot] Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ a billing-specific tool wrapper.
The billing usage/spend tools accept `usage_types` as an array of strings.
Their field description lists the accepted identifiers from `ee/billing/billing_types.py`, through the generated API schema.
The MCP client JSON-encodes the array for the HTTP API.
When both dates are omitted, the shared billing request serializer defaults usage/spend reads to the last 30 complete UTC days, ending yesterday.
Explicit date ranges are unchanged; a start date without an end date still ends today.
This also applies to the organization usage/spend time-series endpoints and CSV exports.
The billing overview, usage, and spend tools do not need a rollout flag.
API scopes and billing access checks still apply.

Expand Down
28 changes: 24 additions & 4 deletions ee/api/billing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import json
from collections.abc import Callable, Sequence
from datetime import timedelta
from typing import Any, NoReturn, Optional, cast
from zoneinfo import ZoneInfo

Expand Down Expand Up @@ -351,8 +352,23 @@ class BillingUsageRequestSerializer(serializers.Serializer):
Only responsible for parsing dates, passes through other params.
"""

start_date = serializers.CharField(required=False, allow_blank=True, allow_null=True)
end_date = serializers.CharField(required=False, allow_blank=True, allow_null=True)
start_date = serializers.CharField(
required=False,
allow_blank=True,
allow_null=True,
help_text=(
'Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.'
),
)
end_date = serializers.CharField(
required=False,
allow_blank=True,
allow_null=True,
help_text=(
"End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, "
"or today if only start_date is provided."
),
)
usage_types = serializers.CharField(
required=False,
allow_blank=True,
Expand Down Expand Up @@ -436,8 +452,12 @@ def validate_end_date(self, value: Optional[str]) -> Optional[str]:
return self._parse_date(value, "end_date")

def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
if attrs.get("start_date") and not attrs.get("end_date"):
attrs["end_date"] = timezone.now().date().isoformat()
today_utc = timezone.now().astimezone(ZoneInfo("UTC")).date()
if not attrs.get("start_date") and not attrs.get("end_date"):
attrs["start_date"] = (today_utc - timedelta(days=30)).isoformat()
attrs["end_date"] = (today_utc - timedelta(days=1)).isoformat()
elif attrs.get("start_date") and not attrs.get("end_date"):
attrs["end_date"] = today_utc.isoformat()
return attrs

def validate_usage_types(self, value: Optional[str]) -> Optional[str]:
Expand Down
44 changes: 39 additions & 5 deletions ee/api/test/test_billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,7 @@ def test_every_billing_action_is_partner_locked_or_explicitly_exempt(self) -> No
assert unlocked == self.READ_ONLY_ACTIONS | self.UNLOCKED_WRITE_ACTIONS


class TestBillingUsageRequestSerializer(TestCase):
class TestBillingUsageRequestSerializer(SimpleTestCase):
def test_valid_dates(self):
serializer = BillingUsageRequestSerializer(data={"start_date": "2025-01-01", "end_date": "2025-01-31"})
self.assertTrue(serializer.is_valid(), serializer.errors)
Expand Down Expand Up @@ -1423,11 +1423,30 @@ def test_accepts_every_breakdown_the_spend_read_serves(self, _case_name: str, va
serializer = BillingUsageRequestSerializer(data={"breakdowns": value})
self.assertTrue(serializer.is_valid(), serializer.errors)

def test_empty_and_null_dates_are_valid(self):
serializer = BillingUsageRequestSerializer(data={"start_date": "", "end_date": None})
@parameterized.expand(
[
("missing", {}),
("empty", {"start_date": "", "end_date": ""}),
("null", {"start_date": None, "end_date": None}),
("empty_start", {"start_date": ""}),
("null_end", {"end_date": None}),
]
)
@time_machine.travel("2025-02-15T00:30:00+14:00", tick=False)
def test_missing_empty_and_null_dates_default_to_last_30_complete_utc_days(
self, _case_name: str, data: dict[str, str | None]
) -> None:
serializer = BillingUsageRequestSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
self.assertEqual(serializer.validated_data["start_date"], "2025-01-15")
self.assertEqual(serializer.validated_data["end_date"], "2025-02-13")

@time_machine.travel("2025-02-15", tick=False)
def test_end_date_without_start_date_is_preserved(self) -> None:
serializer = BillingUsageRequestSerializer(data={"end_date": "2025-02-14"})
self.assertTrue(serializer.is_valid(), serializer.errors)
self.assertIsNone(serializer.validated_data.get("start_date"))
self.assertIsNone(serializer.validated_data.get("end_date"))
self.assertNotIn("start_date", serializer.validated_data)
self.assertEqual(serializer.validated_data["end_date"], "2025-02-14")


class TestBillingUpstreamValidationErrors(SimpleTestCase):
Expand Down Expand Up @@ -1617,6 +1636,21 @@ def test_get_usage_success(self, mock_get_usage_data):
# No teams_map: names are put into the response on the way out.
self.assertNotIn("teams_map", passed_params)

@parameterized.expand([("usage",), ("spend",)])
@time_machine.travel("2025-02-15T00:30:00+14:00", tick=False)
def test_usage_and_spend_default_date_range_is_sent_to_billing(self, endpoint: str) -> None:
manager_method = f"ee.billing.billing_manager.BillingManager.get_{endpoint}_data"
mock_data = self.MOCK_USAGE_DATA if endpoint == "usage" else self.MOCK_SPEND_DATA

with patch(manager_method, return_value=mock_data) as mock_fetch:
response = self.client.get(f"/api/billing/{endpoint}/")

self.assertEqual(response.status_code, status.HTTP_200_OK)
mock_fetch.assert_called_once()
passed_params = mock_fetch.call_args[0][1]
self.assertEqual(passed_params["start_date"], "2025-01-15")
self.assertEqual(passed_params["end_date"], "2025-02-13")

@staticmethod
def _billing_refusal(upstream_status: int, body: object) -> Exception:
# The shape handle_billing_service_error raises: the status in the message, the parsed body third.
Expand Down
20 changes: 12 additions & 8 deletions frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,22 @@ snapshots:
hash: v1.k794b7964.c18261f8a8a79a5e0902ddce51785b490a0989efa6795851640fc3652f196e80.YIgWf5MpUsQDOzGjlEXNDlIrEyg9GEBXU5HZn8A3GNA
components-account-assignment-filter--unassigned-only--light:
hash: v1.k794b7964.4144ad0cfd0b53bd92cb7360bc27c693863338b5d393d69ddd900c542bc6f382.nk0C9Ks32zxldf1ffLBSBoB2xP_PQEKlyBbBOiiNP1c
components-account-project-switcher--no-pending-invite--dark:
hash: v1.k794b7964.ddbb5d27187cb1eb9341734e0b780c762cdfbc09aae81833c970369bd7ad4f32.3HNFMfb4Lflcg3uKD1-KNl09d3leMhmfuhR2WVLjLRE
components-account-project-switcher--no-pending-invite--light:
hash: v1.k794b7964.3f8d66ba04469cdf3e92d78483c58125e84531888eedde628469fde5e9e39a08.MhOzPyXhl7CeOLA5Em19cktkdgffO_BL7CojWkngtMQ
components-account-org-switcher--no-pending-invite--dark:
hash: v1.k794b7964.4388a2607493bd36d9bc8960e2db7720df10102c5e2675bd7b5043f32519aed2.vrUvJ1500oMMFj1Gdc8hWdOaDEMfJRLhHNevwJW_ShQ
components-account-org-switcher--no-pending-invite--light:
hash: v1.k794b7964.db6c1002f011be274c4d274319f422724d8ef4b9c56f7f2f0f29ce23268b1f9b.G8SN1NyxYZSFc-HXe2Jhp1J-2szjpnPFrcuzD_UDLxw
components-account-org-switcher--with-pending-invite--dark:
hash: v1.k794b7964.7e7973392507a753037f4f094c2610038922d5f7c24f53cb34e5eb382847f834.byvdYZq4qgwYwUrcp82ijCWn4wDtvKVHr5qjj0LN3Os
components-account-org-switcher--with-pending-invite--light:
hash: v1.k794b7964.a9b3da4207480a66210bba2a464ba36ec9088faba11571c76a3836bb8ec3224e.pynJGDeHW4VhZJ_1FDVwCNbJc7rqGK17NwwSmkRqg2I
components-account-project-switcher--default--dark:
hash: v1.k794b7964.ddbb5d27187cb1eb9341734e0b780c762cdfbc09aae81833c970369bd7ad4f32.3WklDJAVVmzyrW0Qg1WBXMN0EamqUuc7HcYoGYBjMec
components-account-project-switcher--default--light:
hash: v1.k794b7964.3f8d66ba04469cdf3e92d78483c58125e84531888eedde628469fde5e9e39a08.yksh25-a4q1Q-SmXQSq_IMBe7LNW0d2waKZw_tDj0_A
components-account-project-switcher--with-data-freshness--dark:
hash: v1.k794b7964.33311041a0feedba12322f5c99b05b91db51c7f26793b837fce7c0666c5ef6f5.EPYhVoP42oDcDI9m3qloUEuvND0yjchkisUylND2Y-s
components-account-project-switcher--with-data-freshness--light:
hash: v1.k794b7964.825e1ea3745d1f526c46a4d7b365778ce2d3307933461503fca588f68916a3a9.6Bpu71WRfMaGy8N4X06ixaXTIYkj-4jg44eVXhgWkCw
components-account-project-switcher--with-pending-invite--dark:
hash: v1.k794b7964.b0c1c0b0b8d1a0c77cd873a1cc0b391924cd64ff7de46b1b4d309a50d98eca8a.ddWvJC0f1_fBpYhfDKtUrKeuZSrY5xR8ViHWVdiIfqk
components-account-project-switcher--with-pending-invite--light:
hash: v1.k794b7964.769ba5491bf515d4c18234cbb1b0965e4af8f190b5823914c8955194bb28d356.5QmxWM1jcZVZCAYm6oCJrI2XXnzoIHP19CyfFNvmiog
components-action-step-description--step-variants--dark:
hash: v1.k794b7964.19f9bee33652dbf2425e6df03ae732ed625840c249efc00589e0a1c0457b45d4.rVZ9Ek86XyAHV2eXwI_PDDTdDVv0QnEup4nMHmBjRKk
components-action-step-description--step-variants--light:
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/components/Account/NewAccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
sideOffset?: number
}

export function NewAccountMenu({

Check warning on line 58 in frontend/src/lib/components/Account/NewAccountMenu.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`NewAccountMenu` has cyclomatic complexity 14 (warn >10)
isLayoutNavCollapsed = false,
renderTrigger,
side,
Expand Down Expand Up @@ -134,7 +134,7 @@
)}
{hasPendingInvites && (
<PendingInviteDot
className={isLayoutNavCollapsed ? 'absolute top-0.5 right-0.5' : 'mr-0.5'}
className={isLayoutNavCollapsed ? 'absolute top-0 right-0' : 'ml-1 mr-0.5'}
/>
)}
</ButtonPrimitive>
Expand Down Expand Up @@ -197,7 +197,6 @@
<span className="truncate font-semibold">
{currentTeam ? projectNameWithoutFirstEmoji : 'Select project'}
</span>
{hasPendingInvites && <PendingInviteDot className="mr-0.5" />}
<MenuOpenIndicator intent="sub" className="ml-auto" />
</ButtonPrimitive>
}
Expand Down Expand Up @@ -307,6 +306,7 @@
? currentOrganization.name
: 'Select organization'}
</span>
{hasPendingInvites && <PendingInviteDot className="mr-0.5" />}
<MenuOpenIndicator intent="sub" className="ml-auto" />
</ButtonPrimitive>
}
Expand Down
58 changes: 58 additions & 0 deletions frontend/src/lib/components/Account/OrgSwitcher.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MOCK_DEFAULT_ORGANIZATION, MOCK_DEFAULT_TEAM, MOCK_DEFAULT_USER } from 'lib/api.mock'

import type { Meta, StoryObj } from '@storybook/react'

import { useStorybookMocks } from '~/mocks/browser'

import { OrgSwitcher } from './OrgSwitcher'

type StoryProps = { hasPendingInvite: boolean }

const PENDING_INVITE = {
id: '018f0000-0000-0000-0000-000000000001',
target_email: MOCK_DEFAULT_USER.email,
organization_id: '018f0000-0000-0000-0000-00000000abcd',
organization_name: 'Acme Corp',
created_at: '2026-04-17T12:00:00Z',
}

const meta: Meta<(props: StoryProps) => JSX.Element> = {
title: 'Components/Account/Org Switcher',
parameters: {
layout: 'centered',
viewMode: 'story',
},
render: ({ hasPendingInvite }: StoryProps) => {
useStorybookMocks({
get: {
'/api/users/@me/': () => [
200,
{
...MOCK_DEFAULT_USER,
pending_invites: hasPendingInvite ? [PENDING_INVITE] : [],
},
],
'/api/organizations/@current/': () => [200, MOCK_DEFAULT_ORGANIZATION],
'/api/environments/@current/': () => [200, MOCK_DEFAULT_TEAM],
'/api/projects/@current/': () => [200, MOCK_DEFAULT_TEAM],
},
})

return (
<div className="w-[340px] border border-primary rounded bg-surface-primary">
<OrgSwitcher dialog />
</div>
)
},
}
export default meta

type Story = StoryObj<(props: StoryProps) => JSX.Element>

export const NoPendingInvite: Story = {
args: { hasPendingInvite: false },
}

export const WithPendingInvite: Story = {
args: { hasPendingInvite: true },
}
Loading
Loading