Skip to content

Commit 3cab802

Browse files
committed
Merge main, resolve comment conflict
2 parents cba7b00 + 7cc14b7 commit 3cab802

5 files changed

Lines changed: 1357 additions & 9 deletions

File tree

sidemantic-schema.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,22 @@
642642
"description": "SQL expression or formula (accepts 'expr' as alias)",
643643
"title": "Sql"
644644
},
645+
"steps": {
646+
"anyOf": [
647+
{
648+
"items": {
649+
"type": "string"
650+
},
651+
"type": "array"
652+
},
653+
{
654+
"type": "null"
655+
}
656+
],
657+
"default": null,
658+
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
659+
"title": "Steps"
660+
},
645661
"time_offset": {
646662
"anyOf": [
647663
{
@@ -1633,6 +1649,22 @@
16331649
"description": "SQL expression or formula (accepts 'expr' as alias)",
16341650
"title": "Sql"
16351651
},
1652+
"steps": {
1653+
"anyOf": [
1654+
{
1655+
"items": {
1656+
"type": "string"
1657+
},
1658+
"type": "array"
1659+
},
1660+
{
1661+
"type": "null"
1662+
}
1663+
],
1664+
"default": null,
1665+
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
1666+
"title": "Steps"
1667+
},
16361668
"time_offset": {
16371669
"anyOf": [
16381670
{
@@ -2387,6 +2419,22 @@
23872419
"description": "SQL expression or formula (accepts 'expr' as alias)",
23882420
"title": "Sql"
23892421
},
2422+
"steps": {
2423+
"anyOf": [
2424+
{
2425+
"items": {
2426+
"type": "string"
2427+
},
2428+
"type": "array"
2429+
},
2430+
{
2431+
"type": "null"
2432+
}
2433+
],
2434+
"default": null,
2435+
"description": "N-step funnel filter expressions (overrides base_event/conversion_event)",
2436+
"title": "Steps"
2437+
},
23902438
"time_offset": {
23912439
"anyOf": [
23922440
{

sidemantic/adapters/sidemantic.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ def _parse_model(self, model_def: dict) -> Model | None:
297297
base_event=measure_def.get("base_event"),
298298
conversion_event=measure_def.get("conversion_event"),
299299
conversion_window=measure_def.get("conversion_window"),
300+
steps=measure_def.get("steps"),
300301
offset_window=measure_def.get("offset_window"),
301302
# Retention parameters
302303
cohort_event=measure_def.get("cohort_event"),
@@ -420,6 +421,7 @@ def _parse_metric(self, metric_def: dict) -> Metric | None:
420421
base_event=metric_def.get("base_event"),
421422
conversion_event=metric_def.get("conversion_event"),
422423
conversion_window=metric_def.get("conversion_window"),
424+
steps=metric_def.get("steps"),
423425
offset_window=metric_def.get("offset_window"),
424426
cohort_event=metric_def.get("cohort_event"),
425427
activity_event=metric_def.get("activity_event"),
@@ -588,6 +590,8 @@ def _export_model(self, model: Model) -> dict:
588590
measure_def["conversion_event"] = measure.conversion_event
589591
if measure.conversion_window:
590592
measure_def["conversion_window"] = measure.conversion_window
593+
if measure.steps:
594+
measure_def["steps"] = measure.steps
591595
if measure.offset_window:
592596
measure_def["offset_window"] = measure.offset_window
593597
# Retention parameters
@@ -678,6 +682,8 @@ def _export_metric(self, measure: Metric, graph) -> dict:
678682
result["conversion_event"] = measure.conversion_event
679683
if measure.conversion_window:
680684
result["conversion_window"] = measure.conversion_window
685+
if measure.steps:
686+
result["steps"] = measure.steps
681687
if measure.offset_window:
682688
result["offset_window"] = measure.offset_window
683689
if measure.cohort_event:

sidemantic/core/metric.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,16 @@ def validate_type_specific_fields(self):
200200
if self.type == "conversion":
201201
if not self.entity:
202202
raise ValueError("conversion metric requires 'entity' field")
203-
if not self.base_event:
204-
raise ValueError("conversion metric requires 'base_event' field")
205-
if not self.conversion_event:
206-
raise ValueError("conversion metric requires 'conversion_event' field")
203+
if self.steps:
204+
if len(self.steps) < 2:
205+
raise ValueError("conversion metric 'steps' requires at least 2 steps")
206+
if self.conversion_window:
207+
raise ValueError(
208+
"conversion metric cannot specify both 'steps' and 'conversion_window'; "
209+
"conversion_window is only supported for base_event/conversion_event funnels"
210+
)
211+
elif not self.base_event or not self.conversion_event:
212+
raise ValueError("conversion metric requires 'steps' or both 'base_event' and 'conversion_event'")
207213
if self.type == "retention":
208214
if not self.entity:
209215
raise ValueError("retention metric requires 'entity' field")
@@ -256,6 +262,9 @@ def validate_type_specific_fields(self):
256262
base_event: str | None = Field(None, description="Starting event filter")
257263
conversion_event: str | None = Field(None, description="Target event filter")
258264
conversion_window: str | None = Field(None, description="Conversion time window")
265+
steps: list[str] | None = Field(
266+
None, description="N-step funnel filter expressions (overrides base_event/conversion_event)"
267+
)
259268

260269
# Retention parameters
261270
cohort_event: str | None = Field(

0 commit comments

Comments
 (0)