Skip to content

feat: Store users by package for targeting#81

Closed
ohalushchak-exadel wants to merge 5 commits intomainfrom
users-by-package
Closed

feat: Store users by package for targeting#81
ohalushchak-exadel wants to merge 5 commits intomainfrom
users-by-package

Conversation

@ohalushchak-exadel
Copy link
Copy Markdown
Collaborator

Summary

Replaces the user → segments audience model with a hash(packageID) → {hash(userToken): intent} HSET model, and optimises identity evaluation to a fixed number of Valkey round-trips regardless of package count.

Privacy: package-scoped audience keys

Previously, audience membership was stored per-user (user:profile:<hash> → JSON blob of segments). Any caller who knew a user token hash could read that user's full segment membership across all sellers.

Audience data is now stored per-package: audience:<hash(packageID)> → HSET where each field is hash(userToken) and each value is the intent score. A caller can only derive the key if they know the package ID, so sellers are isolated from each other's audience data. User tokens are still hashed before use.

Write API

PackageIdentityConfig.TargetSegments []string is replaced by Audience bool.

New Engine write methods (all hash-based, all tokens hashed internally):

Method Description
SetPackageUser(ctx, packageID, userToken, intent) Single user → single package
AddPackageUsers(ctx, packageID, users map[string]float64) Batch users → single package
RemovePackageUsers(ctx, packageID, userTokens []string) Remove specific users from a package
DeletePackageUsers(ctx, packageID) Delete an entire package audience
MSetPackageUsers(ctx, packages map[string]map[string]float64) Batch users → multiple packages
MDeletePackageUsers(ctx, packageIDs []string) Delete multiple package audiences (single DEL round-trip)

Read path: N HMGET → 1 pipelined round-trip

EvaluateIdentityResolved previously issued one HMGET per audience-gated package. The new Store.HMGetBatch(keys, fields) method collects all audience keys before the evaluation loop and fetches them in a single pipelined call. In Valkey this is one TCP round-trip regardless of how many packages are audience-gated.

Bug fix

MockStore.Del and MDel previously only cleared m.strings. They now clear all data structures (strings, sets, zsets, hsets, expiry), so DeletePackageUsers and MDeletePackageUsers work correctly in tests.

Adds DeleteUserProfile and DeleteUserProfiles to Engine as counterparts to the existing setters, enabling callers to remove user segment profiles from the store.

  To support this, Del and MDel are added to the Store interface with corresponding implementations in MockStore and valkeystore.Store. The Valkey/Redis implementation uses a single DEL call for both the single and batch cases, since the command natively accepts multiple keys.
## Summary

Replaces the `user → segments` audience model with a `hash(packageID) → {hash(userToken): intent}` HSET model, and optimises identity evaluation to a fixed number of Valkey round-trips regardless of package count.

### Privacy: package-scoped audience keys

Previously, audience membership was stored per-user (`user:profile:<hash>` → JSON blob of segments). Any caller who knew a user token hash could read that user's full segment membership across all sellers.

Audience data is now stored per-package: `audience:<hash(packageID)>` → HSET where each field is `hash(userToken)` and each value is the intent score. A caller can only derive the key if they know the package ID, so sellers are isolated from each other's audience data. User tokens are still hashed before use.

### Write API

`PackageIdentityConfig.TargetSegments []string` is replaced by `Audience bool`.

New Engine write methods (all hash-based, all tokens hashed internally):

| Method | Description |
|---|---|
| `SetPackageUser(ctx, packageID, userToken, intent)` | Single user → single package |
| `AddPackageUsers(ctx, packageID, users map[string]float64)` | Batch users → single package |
| `RemovePackageUsers(ctx, packageID, userTokens []string)` | Remove specific users from a package |
| `DeletePackageUsers(ctx, packageID)` | Delete an entire package audience |
| `MSetPackageUsers(ctx, packages map[string]map[string]float64)` | Batch users → multiple packages |
| `MDeletePackageUsers(ctx, packageIDs []string)` | Delete multiple package audiences (single `DEL` round-trip) |

### Read path: N HMGET → 1 pipelined round-trip

`EvaluateIdentityResolved` previously issued one `HMGET` per audience-gated package. The new `Store.HMGetBatch(keys, fields)` method collects all audience keys before the evaluation loop and fetches them in a single pipelined call. In Valkey this is one TCP round-trip regardless of how many packages are audience-gated.

### Bug fix

`MockStore.Del` and `MDel` previously only cleared `m.strings`. They now clear all data structures (`strings`, `sets`, `zsets`, `hsets`, `expiry`), so `DeletePackageUsers` and `MDeletePackageUsers` work correctly in tests.
@ohalushchak-exadel ohalushchak-exadel marked this pull request as ready for review April 23, 2026 18:06
@bokelley
Copy link
Copy Markdown
Contributor

from a security perspective packages are on the line protocol back and forth from the agent to the ad server at least in some form, so I'm not sure this is more secure. I could ping the server with package IDs that I have seen and get the same information out, right?

@bokelley
Copy link
Copy Markdown
Contributor

this also loses the ability to add a value to a segment, which is useful for minimizing the number of categorical segments - think age:12 vs age1, age2, age3, ...

@bokelley
Copy link
Copy Markdown
Contributor

also doesn't this have to then be seller_url+package_id

@ohalushchak-exadel ohalushchak-exadel deleted the users-by-package branch April 28, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants