Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
33f19dd
fix(signup): keep a digits-only name a string on the confirm page
phillram Oct 21, 2026
f27f3fd
fix(signup): read the confirm page prefill from the raw query string
phillram Oct 21, 2026
ea44998
feat(hogli): stamp session, command kind, and rerun on telemetry events
gantoine Oct 22, 2026
b9db345
fix(hogli): lock telemetry state and recheck recycled leader pids
gantoine Oct 22, 2026
590d063
feat(hogql): run reads of events_json on the events cluster
aspicer Oct 23, 2026
2f6343d
chore: update OpenAPI generated types
tests-posthog[bot] Oct 23, 2026
dc0964f
test(mcp): update unit test snapshots
aspicer Oct 23, 2026
64eda59
fix(hogql): keep async queries and cohort inserts on the events route
aspicer Oct 23, 2026
1ecad18
chore(tests): warm the events cluster host setting before each test
aspicer Oct 23, 2026
099d6e4
feat(hogql): route the temporal jobs' events_json reads to the events…
aspicer Oct 23, 2026
e9bb68d
fix(notebooks): read the frame's route off the context the printer used
aspicer Oct 23, 2026
57e52d5
chore(hogql): name the pinned workloads and stop stripping the remove…
aspicer Oct 23, 2026
02d2262
fix(hogli): classify e2e test runs with the test runner's dispatch rules
gantoine Oct 23, 2026
bbaf711
chore: merge master into native events routing
aspicer Oct 24, 2026
72bdf49
feat(hogql): switch the native events table per person
aspicer Oct 23, 2026
1b4f8b6
chore: update OpenAPI generated types
tests-posthog[bot] Oct 23, 2026
e297b5b
chore(mcp): update tool schema snapshots for the modifier description
aspicer Oct 23, 2026
e291bda
fix(hogql): keep the person switch on streamed dashboards and precomp…
aspicer Oct 24, 2026
abf38f0
chore(hogql): type-ignore the streamed response in the routing test
aspicer Oct 24, 2026
e333c40
fix(hogql): name the events table in a precompute job without resolvi…
aspicer Oct 24, 2026
1a1bf79
fix(hogql): build an automatic precompute job against the table its i…
aspicer Oct 24, 2026
1a48ad2
fix(hogql): preserve the events table across precompute rebuilds
aspicer Oct 24, 2026
88d6045
fix(marketing): key the revalidation debounce tests on the pinned query
aspicer Oct 24, 2026
4476964
fix(data-modeling): retry trino cluster faults that are not the model…
sakce Oct 25, 2026
b91456f
fix(data-modeling): mark cancelled trino shadow jobs as cancelled
sakce Oct 25, 2026
c7c0bb8
fix(data-modeling): give trino shadow runs room to finish the daily dag
sakce Oct 25, 2026
7e0fa92
chore(data-modeling): assert the 23h trino shadow timeout in the dag …
sakce Oct 25, 2026
04434f1
fix(signals): bound scored buckets query per interval and skip slow t…
posthog[bot] Oct 25, 2026
0010860
chore(flags): move the flag edit history read to the flags facade
gustavohstrassburger Oct 26, 2026
83f54e0
fix(warehouse-sources): sync every help scout conversation
danielcarletti Oct 26, 2026
d291389
ci(images): bind the experiments recalculation activity channel
Z3r0Sum Oct 26, 2026
c1db7ca
ci(images): check the committed policy binds every channel
Z3r0Sum Oct 26, 2026
7856330
test(images): keep the binding test in the file's existing style
Z3r0Sum Oct 26, 2026
8dce059
ci(images): track query execution code for the activity channel
Z3r0Sum Oct 26, 2026
09e0cf0
feat(data-catalog): let editors reassign a metric's owner
thiagosalvatore Oct 26, 2026
040b116
test(mcp): update unit test snapshots
tests-posthog[bot] Oct 26, 2026
17f8db8
fix(data-catalog): read the metric owner email through get_attribute
thiagosalvatore Oct 26, 2026
07b5e2a
fix(data-catalog): align the owner picker with the other metric fields
thiagosalvatore Oct 26, 2026
1f14575
ci(images): publish the activity worker on shared model and query cha…
Z3r0Sum Oct 26, 2026
c5486a3
feat(data-catalog): change a metric's owner from the metrics list
thiagosalvatore Oct 26, 2026
41604a8
fix(data-catalog): give the owner picker its hover padding back
thiagosalvatore Oct 26, 2026
09b5be9
chore(data-catalog): cover owner assignment through metric create
thiagosalvatore Oct 26, 2026
e86e0e7
feat(data-catalog): change the owner of many metrics at once
thiagosalvatore Oct 26, 2026
9ee12a2
fix(data-catalog): address review findings on the bulk owner change
thiagosalvatore Oct 26, 2026
9605f8c
Merge remote-tracking branch 'origin/master' into aspicer/events-clus…
aspicer Oct 26, 2026
e5194a5
feat(signals): persist guarded monitoring evaluations
mikaylathompson Oct 26, 2026
15331ff
fix(signals): register monitoring result tenant rules
mikaylathompson Oct 26, 2026
16ef97f
feat(alerts): create and delete alerts that have no source row
jzhu13 Oct 26, 2026
14fd789
fix(data-modeling): only match the trino gateway's own error envelope
sakce Oct 25, 2026
0003fac
fix(alerts): remove native destinations in the delete transaction
jzhu13 Oct 26, 2026
14855e7
fix(data-modeling): add a 23h duration bucket for trino shadow runs
sakce Oct 26, 2026
34bf2fd
feat(hogql): start native events reads on the main cluster again
aspicer Oct 26, 2026
ad4df73
fix(lemon-ui): let the wheel scroll a modal with nested content
phillram Oct 26, 2026
9a63ad5
fix(hogli): hash agent session ids with a local salt
gantoine Oct 26, 2026
6e08aa5
Merge remote-tracking branch 'origin/master' into dc-k/77951-helpscou…
danielcarletti Oct 26, 2026
ca24658
fix(warehouse-sources): stop claiming help scout v2 watermark order
danielcarletti Oct 26, 2026
410337a
fix(data-catalog): satisfy the facade and API path rules for bulk own…
thiagosalvatore Oct 26, 2026
e3ee7f9
fix(data-catalog): skip renamed metrics in the bulk owner change
thiagosalvatore Oct 26, 2026
d36d22c
test(signals): stub scored_buckets in the slow team selection test
posthog[bot] Oct 26, 2026
ecbfaef
feat(data-warehouse): add 8 missing tables to the FusionAuth source
Gilbert09 Oct 26, 2026
5c68f3b
feat(signals): let report actions take a free-text message, and add a…
posthog[bot] Oct 26, 2026
ddc98c5
fix(hogql): pin the preaggregated transform tests to the legacy event…
aspicer Oct 26, 2026
672979f
Merging 14fd7894ed0c313567b6f2a3d92e85bbfaff428b into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
3a3dcd1
Merging 0010860af6c23ecbc68387ffa347b80a63799267 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
affdbf4
Merging 14855e723a7d4d84203db8b7e43a7bd2cfa4f0f3 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
e865368
chore: update OpenAPI generated types
tests-posthog[bot] Oct 26, 2026
8767c98
Merging affdbf40c985434f35cc2873702940a6d672d3e1 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
d05b04b
Merging ecbfaef1699e8a3ea35cda0816e7da492e36800a into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
97f5db7
Merging d05b04b1f3490e60d27ca830ea155faf6be8d964 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
f8c36c3
Merging d36d22cd0f1724fe4f323fa24e6c30fd091b81ba into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
d0659ff
chore(signals): update the report action run tool schema snapshot
posthog[bot] Oct 26, 2026
bd2a952
Merging f8c36c3543b5e7423e738fbd002ee51a6e41d2cc into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
23559ae
Merging ad4df73b5b10a3feaee2a4f05df5d85a8072a4f0 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
fe07fc7
Merging ddc98c53438e6f45a6cb24ca3b82d8c1b8836f93 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
7222067
Merging 9a63ad55af571f9620bec43d8d37287db66a4583 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
bcc4d0b
chore(ci): rerun checks after a hung depot job
jzhu13 Oct 26, 2026
79ef477
Merging 7222067a54eb29d9a99d7cd72134e48a09151cba into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
50a482a
Merging e3ee7f9e2a051084303ab1bf53feff1d0c536783 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
724b532
Merging 15331ff82535d9149f72e0c4d8dc61d00fda3c91 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
9811d24
Merging 724b532581075d16f1f996db2584c756a8b77753 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
81e0952
Merging d0659ffbf5cf964b3d3dd0c928c1421a021d0655 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
e8e24a3
Merging 81e09521001227b9f8557e94407b8be5e0816e00 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
80a919d
Merging 81e09521001227b9f8557e94407b8be5e0816e00 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
8b73a14
Merging ca246583d9eae4ceaa50ac451db3c99e062dc2b5 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
e6a0dbd
Merging bcc4d0bc7a777926e79191c339c9e6df2cedd8b9 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
6c878d6
Merging 81e09521001227b9f8557e94407b8be5e0816e00 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
3c39986
Merging 8b73a14b5149d8f2e9d0157821499ac7dcba2139 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
bf7c416
Merging e6a0dbd79fa2758d01a78e1df248a2a39922e304 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
b215fc8
Merging f27f3fdd5edf755fc0ecc0f9193cc6abe5dc83de into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 2026
eb98f94
Merging 1f145757b023ea898f4877aedaf0cb7cf2d94e07 into trunk-temp/pr-1…
trunk-io[bot] Oct 26, 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 @@ -89,7 +89,7 @@ describe('shared-image policy and legacy projection', () => {
const inlineRules = yaml.load(inline!) as Record<string, Paths | null>
const policy = productionPolicy()
const projected = yaml.load(projectLegacyFilters(policy, inline!)) as Record<string, Paths>
expect(Object.keys(projected)).toEqual(Object.keys(inlineRules))
expect(Object.keys(projected)).toEqual(Object.keys({...inlineRules, ...policy.filters}))
for (const [name, rule] of Object.entries(inlineRules)) {
expect({[name]: projected[name]}).toEqual({[name]: rule === null ? policy.filters[name] : rule})
}
Expand Down Expand Up @@ -131,6 +131,5 @@ describe('shared-image policy and legacy projection', () => {
const result = filter.match(files)
expect(result.dataWarehouseMetadata).toEqual([files[0]])
expect(result.dataWarehouse).toEqual(files)
expect(current.images['posthog-cloud'].channels).toEqual({})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('shared-image policy and legacy projection', () => {
const inlineRules = yaml.load(inline!) as Record<string, Paths | null>
const policy = productionPolicy()
const projected = yaml.load(projectLegacyFilters(policy, inline!)) as Record<string, Paths>
expect(Object.keys(projected)).toEqual(Object.keys(inlineRules))
expect(Object.keys(projected)).toEqual(Object.keys({...inlineRules, ...policy.filters}))
for (const [name, rule] of Object.entries(inlineRules)) {
expect({[name]: projected[name]}).toEqual({[name]: rule === null ? policy.filters[name] : rule})
}
Expand Down Expand Up @@ -131,6 +131,5 @@ describe('shared-image policy and legacy projection', () => {
const result = filter.match(files)
expect(result.dataWarehouseMetadata).toEqual([files[0]])
expect(result.dataWarehouse).toEqual(files)
expect(current.images['posthog-cloud'].channels).toEqual({})
})
})
31 changes: 21 additions & 10 deletions .github/actions/shared-image-releases/__tests__/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ jest.mock('../src/shared-image-command')

import * as core from '@actions/core'
import * as path from 'path'
import {mkdtempSync, rmSync, writeFileSync} from 'fs'
import {mkdirSync, mkdtempSync, rmSync, writeFileSync} from 'fs'
import {tmpdir} from 'os'
import {command} from '../src/shared-image-command'
import {run} from '../src/inspect'
import {POLICY_PATH} from '../src/shared-image-releases'
import * as publisher from '../src/shared-image-publisher'

jest.mock('@actions/core', () => ({getInput: jest.fn(), setOutput: jest.fn(), setFailed: jest.fn()}))
Expand Down Expand Up @@ -42,15 +43,25 @@ describe('inspect action inputs and outputs', () => {
})

test('an empty configured channel set emits false and the resolved empty map', async () => {
jest.mocked(core.getInput).mockImplementation(name => {
if (name === 'working-directory') return path.join(__dirname, '../../../..')
if (name === 'shared-image-policy') return ''
return inputs[name] || ''
})
await run()
expect(core.setFailed).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenCalledWith('channels', 'false')
expect(core.setOutput).toHaveBeenCalledWith('bindings', '{"posthog-cloud":{}}')
const scratch = mkdtempSync(path.join(tmpdir(), 'shared-image-empty-'))
try {
const policyFile = path.join(scratch, POLICY_PATH)
mkdirSync(path.dirname(policyFile), {recursive: true})
writeFileSync(policyFile, JSON.stringify({
version: 1, filters: {}, images: {'posthog-cloud': {channels: {}}}
}))
jest.mocked(core.getInput).mockImplementation(name => {
if (name === 'working-directory') return scratch
if (name === 'shared-image-policy') return ''
return inputs[name] || ''
})
await run()
expect(core.setFailed).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenCalledWith('channels', 'false')
expect(core.setOutput).toHaveBeenCalledWith('bindings', '{"posthog-cloud":{}}')
} finally {
rmSync(scratch, {recursive: true, force: true})
}
})

test.each([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import {readFileSync} from 'fs'
import * as path from 'path'
import * as yaml from 'js-yaml'
import {Paths, bindChannels, loadPolicy, policyDigest, serializePolicy} from '../src/shared-image'
import {Paths, POLICY_PATH, bindChannels, loadPolicy, policyDigest, serializePolicy} from '../src/shared-image'

const destination = {repository: 'posthog-temporal-workers/data-warehouse-metadata'}
const source = 'posthog-cloud'
const fixture = readFileSync(path.join(__dirname, '../../paths-filter/__tests__/fixtures/shared-image-policy.yaml'), 'utf8')
const literal = JSON.parse(readFileSync(path.join(__dirname, 'fixtures/shared-image-fingerprints.json'), 'utf8')) as
{paths: Paths; bytes: string; sha256: string}[]
describe('shared-image publication bindings and fingerprints', () => {
test('binds every channel of the committed policy', () => {
const policy = loadPolicy(readFileSync(path.join(__dirname, '../../../..', POLICY_PATH), 'utf8'))
const channels = policy.images['posthog-cloud'].channels
const bindings = bindChannels(policy, 'posthog-cloud')['posthog-cloud']
expect(Object.keys(bindings).sort()).toEqual(Object.keys(channels).sort())
for (const [name, channel] of Object.entries(channels)) {
expect(bindings[name]).toEqual({repository: channel.repository})
}
})

test.each([
['unknown build', fixture.replace('posthog-cloud:', 'unknown-image:'), 'images.unknown-image: unknown build input'],
['prototype build', fixture.replace('posthog-cloud:', '__proto__:'), 'images.__proto__: unknown build input'],
Expand Down
29 changes: 28 additions & 1 deletion .github/shared-image-releases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,34 @@ filters:
- pyproject.toml
- bin/temporal-django-worker
- 'posthog/clickhouse/migrations/**'
experimentsRecalculationActivity:
- 'products/experiments/backend/**'
- 'products/experiments/stats/**'
- 'posthog/hogql*/**'
- 'posthog/clickhouse/**'
- 'posthog/temporal/common/**'
- 'posthog/management/commands/start_temporal_worker.py'
- 'posthog/settings/temporal.py'
- posthog/schema.py
- 'posthog/models/**'
- 'posthog/migrations/**'
- 'products/*/backend/migrations/**'
- posthog/exceptions.py
- 'products/feature_flags/backend/**'
- 'products/cohorts/backend/**'
- 'products/analytics_platform/backend/lazy_computation/**'
- 'products/ai_observability/backend/facade/**'
- 'common/hogvm/**'
- pyproject.toml
- uv.lock
- bin/temporal-django-worker
- manage.py
- Dockerfile

images:
posthog-cloud:
channels: {}
channels:
experiments-recalculation-activity:
repository: posthog-temporal-workers/experiments-recalculation-activity
filter: experimentsRecalculationActivity
bootstrapPolicyDigest: a06fc277bfb801b6da6406ac2a8d0ed82ac09c13e1ac37234c8a4e01b44b1313
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@ describe('confirmOrganizationLogic', () => {
})

describe('query params', () => {
it('prefills the form from the URL', async () => {
router.actions.push('/organization/confirm-creation', {
email: 'spike@spike.com',
first_name: 'Spike',
organization_name: 'Spikes Inc',
})
it.each([
['Spike', 'Spikes Inc'],
['3130', '2600'],
])('prefills the form from the URL with the name %p', async (firstName, organizationName) => {
// A raw URL, not a params object: kea-router passes object values through untouched, so
// only a real query string parses a name of only digits into a number, which used to crash
router.actions.push(
`/organization/confirm-creation?email=spike@spike.com&first_name=${encodeURIComponent(firstName)}&organization_name=${encodeURIComponent(organizationName)}`
)

await expectLogic(logic)
.toDispatchActions(['setEmail', 'setConfirmOrganizationValues'])
.toMatchValues({
confirmOrganization: expect.objectContaining({
first_name: 'Spike',
organization_name: 'Spikes Inc',
first_name: firstName,
organization_name: organizationName,
}),
email: 'spike@spike.com',
})

expect(logic.values.confirmOrganizationValidationErrors.first_name).toBeUndefined()
expect(logic.values.confirmOrganizationValidationErrors.organization_name).toBeUndefined()
})

it('keeps user input when a later location change re-fires the handler', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export const confirmOrganizationLogic = kea<confirmOrganizationLogicType>([
})),

urlToAction(({ actions, values }) => ({
'/organization/confirm-creation': (_, { email, organization_name, first_name }) => {
'/organization/confirm-creation': (_, __, ___, { search }) => {
// Prefill from the URL once. Later location changes on this same page (for example
// opening the docs side panel) re-fire this handler with empty params, which would
// otherwise wipe what the user has typed.
Expand All @@ -220,15 +220,22 @@ export const confirmOrganizationLogic = kea<confirmOrganizationLogicType>([
}
actions.setHasPrefilled()

// Read the raw query string, because kea-router parses a value that looks like a number
// into one. A name of only digits must stay text for the validators to trim it.
const params = new URLSearchParams(search)
const email = params.get('email')
const organizationName = params.get('organization_name')
const firstName = params.get('first_name')

if (email) {
actions.setEmail(email)
}
const prefill: DeepPartial<ConfirmOrganizationFormValues> = {}
if (organization_name) {
prefill.organization_name = organization_name
if (organizationName) {
prefill.organization_name = organizationName
}
if (first_name) {
prefill.first_name = first_name
if (firstName) {
prefill.first_name = firstName
}
if (Object.keys(prefill).length > 0) {
actions.setConfirmOrganizationValues(prefill)
Expand Down
48 changes: 46 additions & 2 deletions products/alerts_platform/backend/facade/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,24 +979,68 @@ class SourceDelete(Protocol):
def __call__(self, *, team_id: int, legacy_configuration_id: UUID) -> None: ...


class SourceConfigTeamCheck(Protocol):
"""Checks a `source_config` against the project it is written in, which `validate` cannot see.

For example, that its ids name rows of that project. It raises `InvalidPlatformAlertEdit`.
"""

def __call__(self, *, team_id: int, edit: PlatformAlertConfigurationEdit) -> None: ...


class NativeAlertRemoveDestinations(Protocol):
"""Removes the destinations of an alert that is about to be deleted.

The alert's open incidents are closed before this runs, while the destinations their resolves go
to still exist. It runs in the delete's transaction, so a delete that fails keeps them.
"""

def __call__(self, *, team_id: int, configuration_id: UUID) -> None: ...


@frozen
class NativeAlertLifecycle:
"""How the platform creates and deletes alerts of a kind whose source keeps no row for them.

The configuration is the alert's only record, and its own id is the id the alert's destinations
are matched on.
"""

check_interval_minutes: int
remove_destinations: NativeAlertRemoveDestinations


@frozen
class SourceConfigSpec:
"""A source kind's schema for `source_config` and its rules for a whole edit.

`model` validates `source_config`. `validate` raises `InvalidPlatformAlertEdit` for a rule that
spans fields, such as one that reads `enabled`. `mirror` runs in the edit's transaction. Without
`create` or `delete`, the API cannot create or delete alerts of the kind.
spans fields, such as one that reads `enabled`. `check_in_team` runs when a create or an edit
sets `source_config`. `mirror` runs in the edit's transaction. Without
`create` or `delete`, the API cannot create or delete alerts of the kind, unless the kind is
`native`: then the platform creates and deletes them itself, and the source keeps no row.
"""

model: type[BaseModel]
validate: Callable[[PlatformAlertConfigurationEdit], None]
check_in_team: SourceConfigTeamCheck | None = None
mirror: SourceEditMirror | None = None
create: SourceCreate | None = None
delete: SourceDelete | None = None
# Where the source's incident destinations listen for a close, and the alert's page in the
# source's product. A control-plane close needs both to address its resolve.
incident_event_ids: Mapping[str, str] | None = None
alert_url: Callable[[int, UUID], str] | None = None
native: NativeAlertLifecycle | None = None

def __post_init__(self) -> None:
# The source-row hooks write a row a native kind does not have.
if self.native is not None and (self.mirror or self.create or self.delete):
raise ValueError("A native kind keeps no source row, so it cannot register mirror, create or delete.")

@property
def creatable(self) -> bool:
return self.native is not None or self.create is not None


SourceConfigLoader = Callable[[], SourceConfigSpec]
Expand Down
12 changes: 6 additions & 6 deletions products/alerts_platform/backend/logic/control_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class PendingClose:
team_id: int
source_kind: str
configuration_id: UUID
legacy_configuration_id: UUID | None
destination_alert_id: UUID
rows: tuple[PlatformAlertEventRow, ...]


Expand Down Expand Up @@ -90,7 +90,7 @@ def end_firings(configuration: PlatformAlertConfiguration, *, reason: str, now:
team_id=configuration.team_id,
source_kind=configuration.source_kind,
configuration_id=configuration.id,
legacy_configuration_id=configuration.legacy_configuration_id,
destination_alert_id=configuration.destination_alert_id,
rows=rows,
)

Expand Down Expand Up @@ -129,21 +129,21 @@ def _announce(pending: PendingClose, *, inline: bool) -> None:

def _resolve_delivery(pending: PendingClose) -> AlertDeliveryRequest | None:
spec = source_configs.spec(SourceKind(pending.source_kind))
if spec is None or spec.incident_event_ids is None or pending.legacy_configuration_id is None:
if spec is None or spec.incident_event_ids is None:
return None
grouping_keys = [row.grouping_key for row in pending.rows]
return AlertDeliveryRequest(
source=SourceKind(pending.source_kind),
team_id=pending.team_id,
configuration_id=str(pending.configuration_id),
evaluation_key=pending.rows[0].evaluation_key,
# The id the source's destinations are matched on, as on every check's delivery.
destination_alert_id=str(pending.legacy_configuration_id),
# The id the alert's destinations are matched on, as on every check's delivery.
destination_alert_id=str(pending.destination_alert_id),
event_ids_by_kind={},
incident_actions=dict.fromkeys(grouping_keys, IncidentAction.RESOLVE),
sends_messages=False,
event_ids_by_incident_action=dict(spec.incident_event_ids),
source_alert_url=spec.alert_url(pending.team_id, pending.legacy_configuration_id) if spec.alert_url else None,
source_alert_url=spec.alert_url(pending.team_id, pending.destination_alert_id) if spec.alert_url else None,
owned_by_platform=True,
parts=parts_for(len(grouping_keys)),
)
29 changes: 28 additions & 1 deletion products/alerts_platform/backend/logic/platform_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
PlatformAlertUpsert,
PlatformHandback,
PlatformOwnershipHandover,
SourceKind,
source_condition,
)
from products.alerts_platform.backend.facade.enums import PlatformAlertOwner, PlatformAlertState
Expand Down Expand Up @@ -504,7 +505,7 @@
return replace(group, kind=AlertEventKind.CHECK, notified=False, muted_notification=held)


def record_outcomes(team_id: int, outcomes: Sequence[PlatformAlertOutcome], now: datetime) -> int:

Check warning on line 508 in products/alerts_platform/backend/logic/platform_lifecycle.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`record_outcomes` has cyclomatic complexity 13 (warn >10)

Check warning on line 508 in products/alerts_platform/backend/logic/platform_lifecycle.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`record_outcomes` has cyclomatic complexity 13 (warn >10)
"""Persists a batch's decisions and advances each configuration's schedule.

Two statements rather than two per alert, in one transaction, so a crash between them cannot
Expand Down Expand Up @@ -797,6 +798,33 @@
if reenabled:
configuration.next_check_at = now + _REVIVE_FENCE
update_fields.append("next_check_at")
_assign_edit(configuration, edit)
configuration.save(update_fields=update_fields)


def create_native_configuration(
*,
team_id: int,
source_kind: SourceKind,
edit: PlatformAlertConfigurationEdit,
check_interval_minutes: int,
now: datetime,
) -> PlatformAlertConfiguration:
"""Creates an alert the platform owns from the start, for a kind whose source keeps no row for it."""
configuration = PlatformAlertConfiguration(
team_id=team_id,
source_kind=source_kind.value,
check_interval_minutes=check_interval_minutes,
next_check_at=now,
owner=PlatformAlertOwner.PLATFORM,
)
_assign_edit(configuration, edit)
# The model's own save logs the creation.
configuration.save()
return configuration


def _assign_edit(configuration: PlatformAlertConfiguration, edit: PlatformAlertConfigurationEdit) -> None:
configuration.name = edit.name
configuration.enabled = edit.enabled
configuration.source_config = edit.source_config
Expand All @@ -805,7 +833,6 @@
configuration.evaluation_periods = edit.evaluation_periods
configuration.datapoints_to_alarm = edit.datapoints_to_alarm
configuration.cooldown_minutes = edit.cooldown_minutes
configuration.save(update_fields=update_fields)


def end_firing_instances(configuration: PlatformAlertConfiguration, *, now: datetime) -> list[PlatformAlert]:
Expand Down
Loading
Loading