-
Notifications
You must be signed in to change notification settings - Fork 67
feat: impact metrics #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: impact metrics #387
Conversation
Pull Request Test Coverage Report for Build 21248587395Details
💛 - Coveralls |
850ddee to
41adbe3
Compare
8642418 to
5730f0c
Compare
cb22b2e to
8b21608
Compare
f59beb5 to
a637b07
Compare
1cd0a29 to
a9fa478
Compare
sighphyre
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah cool, this shaped up really nicely
| engine: UnleashEngine, | ||
| ) -> None: | ||
| metrics_bucket = engine.get_metrics() | ||
| impact_metrics = engine.collect_impact_metrics() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remember the binding code, this can potentially raise an exception. Think that's pretty unlikely but how do you feel about making this flow not bork metrics if this fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I though it should never happen. But to be safe with some weird internal errors I can wrap it with an error handler
UnleashClient/__init__.py
Outdated
| self.metric_job: Job = None | ||
| self.engine = UnleashEngine() | ||
| self._impact_metrics = ImpactMetrics( | ||
| self.engine, self.unleash_app_name, self.unleash_environment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to do what we do in Node and get this from the token? I'm going to deprecate this environment across the SDKs over the next few months, just a heads up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I remember fixing it in Node so we can do the same here.
UnleashClient/__init__.py
Outdated
| return self._run_state == _RunState.INITIALIZED | ||
|
|
||
| @property | ||
| def impact_metrics(self) -> ImpactMetrics: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, found the Java developer! So I don't think this adds anything to our lives in the current shape. This is necessary pattern in Java but the magic of Python properties is that we can do this later if we ever feel the need without impacting the public API. I would just expose the field directly to be honest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly I didn't want to do this but got inspired by def connection_id(self) which should probably be a regular property
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably should. Dunno why it's like that haha, probably AI code that got a bit too excited
|
|
||
| ``` | ||
|
|
||
| ### Impact metrics |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
| labels = self._resolve_labels(flag_context) | ||
| self._engine.observe_histogram(name, value, labels) | ||
|
|
||
| def _resolve_labels( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is wrong but it does look like "Java developer slipped, fell and landed in the Python runtime" to me. What you have is clear so I'm going to make some recommendations based on what I would do given my Python background and you can pick and choose some or none of this
I think the creation of the dict here is a bit unfortunate. We close over the environment and appName fields but literally only use them here to new this dict up. Smells like a way to work around Python's lack of destructuring. Good news, Python does have destructuring though!
This is also definitely begging to be a list comprehension with destructuring. That may or may not read okay to someone without a Python background but it reads fine to me
This is how I would have done this:
def __init__(self, engine: UnleashEngine, app_name: str, environment: str):
self._engine = engine
self._base_labels = {
"appName": app_name,
"environment": environment,
}
def _variant_label(self, flag_name: str, ctx) -> str:
variant = self._engine.get_variant(flag_name, ctx)
if variant and variant.enabled:
return variant.name
if variant and variant.feature_enabled:
return "enabled"
return "disabled"
def _resolve_labels(
self, flag_context: Optional[MetricFlagContext]
) -> Dict[str, str]:
if not flag_context:
## Just a lil defensive copying so we don't leak mutable state
return dict(self._base_labels)
return {
**self._base_labels,
**{
flag: self._variant_label(flag, flag_context.context)
for flag in flag_context.flag_names
},
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup I like your version more. thanks for pointing it towards more idiomatic python
| """Context for resolving feature flag values as metric labels.""" | ||
|
|
||
| flag_names: List[str] = field(default_factory=list) | ||
| context: Dict[str, Any] = field(default_factory=dict) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting some mixed messages from this field. The class is a context but so is this. Is it contexts all the way down?
Weirdly enough because of this, I have no context for what this actually represents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is consistent with what we called it in Node SDK. This is our user context so that we can evaluate flag_names to enabled/disabled/variant_name with a user context. I agree it may be confusing here since in Node Context has a proper shape:
export interface Context {
[key: string]: string | Date | undefined | number | Properties;
currentTime?: Date;
userId?: string;
sessionId?: string;
remoteAddress?: string;
environment?: string;
appName?: string;
properties?: Properties;
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this SDK uses plain dict for user context everywhere and I don't want to make a big change in this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! It's the actual context. Yep, makes sense, let's keep it the same. Context needs a proper type some day, today is not that day
75d07f3 to
d4b9794
Compare
37db13b to
d41836b
Compare
1cefe13 to
bb4445f
Compare
6445a59 to
a935bd3
Compare
for more information, see https://pre-commit.ci
Description
Adds full impact metrics support to this SDK
This PR is the first example of impact metrics implemented mostly in Yggdrasil, with the SDK doing the wrapper work.
The actual change is <100 LOC. Most of the PR is README and tests.
What the SDK has to do is:
Interesting decisions:
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
Checklist: