Skip to content

Commit 2f3af09

Browse files
committed
transient identifier based on traits, enforce transient traits for all identities, improve typing
1 parent 5437e8e commit 2f3af09

File tree

4 files changed

+44
-20
lines changed

4 files changed

+44
-20
lines changed

api/environments/identities/traits/fields.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
2+
from typing import Any
23

34
from rest_framework import serializers
45

56
from environments.identities.traits.constants import (
67
ACCEPTED_TRAIT_VALUE_TYPES,
78
TRAIT_STRING_VALUE_MAX_LENGTH,
89
)
10+
from environments.sdk.types import SDKTraitValueData
911
from features.value_types import STRING
1012

1113
logger = logging.getLogger(__name__)
@@ -16,7 +18,7 @@ class TraitValueField(serializers.Field):
1618
Custom field to extract the type of the field on deserialization.
1719
"""
1820

19-
def to_internal_value(self, data):
21+
def to_internal_value(self, data: Any) -> SDKTraitValueData:
2022
data_type = type(data).__name__
2123

2224
if data_type not in ACCEPTED_TRAIT_VALUE_TYPES:
@@ -28,7 +30,7 @@ def to_internal_value(self, data):
2830
)
2931
return {"type": data_type, "value": data}
3032

31-
def to_representation(self, value):
33+
def to_representation(self, value: Any) -> Any:
3234
return_value = value.get("value") if isinstance(value, dict) else value
3335

3436
if return_value is None:

api/environments/sdk/services.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import uuid
1+
import hashlib
22
from itertools import chain
3+
from operator import itemgetter
34
from typing import TypeAlias
45

56
from django.utils import timezone
@@ -18,15 +19,16 @@ def get_transient_identity_and_traits(
1819
) -> IdentityAndTraits:
1920
"""
2021
Get a transient `Identity` instance with a randomly generated identifier.
22+
All traits are marked as transient.
2123
"""
2224
return (
2325
(
2426
identity := _get_transient_identity(
2527
environment=environment,
26-
identifier=str(uuid.uuid4()),
28+
identifier=get_transient_identifier(sdk_trait_data),
2729
)
2830
),
29-
identity.generate_traits(sdk_trait_data, persist=False),
31+
identity.generate_traits(_ensure_transient(sdk_trait_data), persist=False),
3032
)
3133

3234

@@ -41,8 +43,7 @@ def get_identified_transient_identity_and_traits(
4143
combined with incoming traits provided to `sdk_trait_data` argument.
4244
All traits constructed from `sdk_trait_data` are marked as transient.
4345
"""
44-
for sdk_trait_data_item in sdk_trait_data:
45-
sdk_trait_data_item["transient"] = True
46+
sdk_trait_data = _ensure_transient(sdk_trait_data)
4647
if identity := Identity.objects.filter(
4748
environment=environment,
4849
identifier=identifier,
@@ -89,6 +90,16 @@ def get_persisted_identity_and_traits(
8990
)
9091

9192

93+
def get_transient_identifier(sdk_trait_data: list[SDKTraitData]) -> str:
94+
return hashlib.sha256(
95+
"".join(
96+
f'{trait["trait_key"]}{trait["trait_value"]["value"]}'
97+
for trait in sorted(sdk_trait_data, key=itemgetter("trait_key"))
98+
).encode(),
99+
usedforsecurity=False,
100+
).hexdigest()
101+
102+
92103
def _get_transient_identity(
93104
environment: Environment,
94105
identifier: str,
@@ -98,3 +109,9 @@ def _get_transient_identity(
98109
environment=environment,
99110
identifier=identifier,
100111
)
112+
113+
114+
def _ensure_transient(sdk_trait_data: list[SDKTraitData]) -> list[SDKTraitData]:
115+
for sdk_trait_data_item in sdk_trait_data:
116+
sdk_trait_data_item["transient"] = True
117+
return sdk_trait_data

api/environments/sdk/types.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from typing_extensions import NotRequired
44

55

6+
class SDKTraitValueData(typing.TypedDict):
7+
type: str
8+
value: str
9+
10+
611
class SDKTraitData(typing.TypedDict):
712
trait_key: str
8-
trait_value: typing.Any
13+
trait_value: SDKTraitValueData
914
transient: NotRequired[bool]

api/tests/integration/environments/identities/test_integration_identities.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import hashlib
12
import json
23
from typing import Any, Generator
34
from unittest import mock
45

56
import pytest
67
from django.urls import reverse
78
from pytest_lazyfixture import lazy_fixture
8-
from pytest_mock import MockerFixture
99
from rest_framework import status
1010
from rest_framework.test import APIClient
1111

@@ -236,12 +236,13 @@ def existing_identity_identifier_data(
236236

237237

238238
@pytest.fixture
239-
def transient_random_identifier(
240-
mocker: MockerFixture,
239+
def transient_identifier(
240+
segment_condition_property: str,
241+
segment_condition_value: str,
241242
) -> Generator[str, None, None]:
242-
uuid_mock = mocker.patch("environments.sdk.services.uuid", autospec=True)
243-
uuid_mock.uuid4.return_value = identifier = "1199c22c-4dcb-4505-9857-5db5f258469c"
244-
yield identifier
243+
return hashlib.sha256(
244+
f"avalue_a{segment_condition_property}{segment_condition_value}".encode()
245+
).hexdigest()
245246

246247

247248
@pytest.mark.parametrize(
@@ -263,17 +264,15 @@ def transient_random_identifier(
263264
pytest.param({"identifier": "unseen"}, "unseen", id="new-identifier"),
264265
pytest.param(
265266
{"identifier": ""},
266-
lazy_fixture("transient_random_identifier"),
267+
lazy_fixture("transient_identifier"),
267268
id="blank-identifier",
268269
),
269270
pytest.param(
270271
{"identifier": None},
271-
lazy_fixture("transient_random_identifier"),
272+
lazy_fixture("transient_identifier"),
272273
id="null-identifier",
273274
),
274-
pytest.param(
275-
{}, lazy_fixture("transient_random_identifier"), id="missing-identifier"
276-
),
275+
pytest.param({}, lazy_fixture("transient_identifier"), id="missing-identifier"),
277276
],
278277
)
279278
def test_get_feature_states_for_identity__segment_match_expected(
@@ -303,7 +302,8 @@ def test_get_feature_states_for_identity__segment_match_expected(
303302
{
304303
"trait_key": segment_condition_property,
305304
"trait_value": segment_condition_value,
306-
}
305+
},
306+
{"trait_key": "a", "trait_value": "value_a"},
307307
],
308308
}
309309
),

0 commit comments

Comments
 (0)