{"id":22109662,"url":"https://github.com/jazcarate/koncierge","last_synced_at":"2026-06-24T22:31:32.101Z","repository":{"id":74110734,"uuid":"300710769","full_name":"jazcarate/koncierge","owner":"jazcarate","description":"Kotlin lib to use in an AB testing service","archived":false,"fork":false,"pushed_at":"2021-10-25T08:57:28.000Z","size":957,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-08T05:03:31.102Z","etag":null,"topics":["ab","kotlin","library","parser","testing","variant"],"latest_commit_sha":null,"homepage":"https://koncierge.florius.com.ar","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jazcarate.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-02T19:00:29.000Z","updated_at":"2021-10-25T08:57:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"3a64a784-87ab-465f-b141-647bacaba775","html_url":"https://github.com/jazcarate/koncierge","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jazcarate/koncierge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazcarate%2Fkoncierge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazcarate%2Fkoncierge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazcarate%2Fkoncierge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazcarate%2Fkoncierge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jazcarate","download_url":"https://codeload.github.com/jazcarate/koncierge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazcarate%2Fkoncierge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34752465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ab","kotlin","library","parser","testing","variant"],"created_at":"2024-12-01T09:36:31.166Z","updated_at":"2026-06-24T22:31:32.081Z","avatar_url":"https://github.com/jazcarate.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# koncierge 🛎\n![tests](https://github.com/jazcarate/koncierge/workflows/tests/badge.svg)\n\nEvaluating AB testing variants, given an experiment definition and some context.\n\n## Rationale\nWe have a microservice that is in charge of knowing whether a user should be part of an experiment or not,\nand if they are, whether they should be in the control group, or the experiment group.\n\nThe reasons why a user might participate or not are very varied, and change drastically from experiment to experiment.\n\nWe want to have the experiment's microservice be somewhat agnostic of the rest of the microservice infrastructure;\nso whoever consumes the information must provide enough information about the user for us to choose what experiments\nthey should be part of.\n\nWe found that knowing if a user participates in an experiment, and it figures out what variant they fall into are really\nsimilar concepts, so we came up with the idea of having _\"variants all the way\"_. So an experiment is really just a variant\non the world. The same syntax can apply to narrow down the focus of the experiment. A good example of this can be found\nin the [Variant all the way](#variant-all-the-way) example.\n\n## Syntax\nWe borrowed heavily for [Mongo's query language](https://docs.mongodb.com/manual/tutorial/query-documents/).\n\n### Operators\nThese are the fields that start with a `$`.\nThere are two families of operators, describe below.\n\n### Context changers\nThese operations change the context, either narrowing down the provided context, or by plucking some information from the `World`.\nThink of them as `:: Context -\u003e Context`.\n\n| Name               | Description                                                                                             | Example                                          |\n|--------------------|---------------------------------------------------------------------------------------------------------| -------------------------------------------------|\n| any non-`$` string | Will dive into the context to the key with the same name. (can deep dive by combining keys with `.`s)   | `{ \"foo.bar\": \"yes\" }`                           |\n| `$rand`            | Chooses a value at random between 0 and 1. Uses the current context as seed[*](#randomness)             | `{ \"$rand\": { \"$gt\": 0.5 } }`                    |\n| `$chaos`           | Chooses a value at random between 0 and 1. This, compared to ☝️does not use the current context       | `{ \"$chaos\": { \"$gt\": 0.5 } }`                    |\n| `$date`            | Changes the context with the current date. Useful when convincing with other comparison operator        | `{ \"$date\": { \"$gt\":  \"2020-01-01 15:00:00\" } }` |\n| `$size`            | Changes the context to the size of the current context. This can be an array length, a string length or an object number of fields, or `0` if `null` | `{ \"$size\": { \"$gt\":  \"1\" } }` |\n| `$not`             | Negates the context.                                                                                    | `{ \"$not\": { \"$gt\":  \"1\" } }` |\n\n#### Evaluators\nThese operations yield weather the context is or not par of the variant.\nThink of them as `:: Context -\u003e Bool`.\n\n| Name               | Description                                                                                             | Example                                          |\n|--------------------|---------------------------------------------------------------------------------------------------------| -------------------------------------------------|\n| `$lt`, `$gt`       | Will be enabled if the context if less than (`lt`) or greater than (`gt`) the value provided [**](#date)| `{ \"$gt\": 0.5 }`                                 |\n| `$or`              | Will be enabled if any of the sub-operators are enabled                                                 | `{ \"$or\": { \"beta\": \"yes\", \"$rand\" : 0.5 } }`    |\n| `$always`          | Will always evaluate to its (boolean) value, regardless of the context. See the [exists](#exists) example| `{ \"$always\": true }`                            |\n| `$any`, `$all`     | Will be enabled if (any/all) the elements in the context (which should be an array) are enabled         | `{ \"$any\": { \"$gt\": 0.5 } }`                     |\n\nWhere a context changer can be nested, evaluators need to be terminal.\n\nEven though there are both `$and` and `$eq` operators; they are rarely used, as they can be expressed more concisely.\nRefer to the [And](#and) example for more information.\n\n## Examples\nYou can play around with different experiments and contexts in our [koncierge playground](https://koncierge-playground.herokuapp.com).\n\n### Simple\nEveryone is participating in the experiment`EXP001`, and only half of the users (chosen at random) will see the experiment.\n```json\n{\n    \"EXP001\": {\n        \"$children\": {\n            \"participating\": { \"$rand\": { \"$gt\": 0.5 } },\n            \"control\": { \"$always\":  true }\n        }\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22$children%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22participating%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$rand%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$gt%22:%200.5%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22control%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$always%22:%20true%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)\n\nThe output for half the user base will be: `EXP001.participating` and `EXP001.control` for the other half.\n\n### Variant all the way\n```json\n{\n    \"EXP001\": {\n        \"$date\": { \"$gt\": \"2020-01-01 15:00:00\" },\n        \"$children\": {\n            \"participating\": { \"$rand\": { \"$gt\": 0.5 } },\n            \"control\": { \"$always\":  true }\n        }\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22$date%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22$gt%22:%20%222020-01-01%2015:00:00%22%0A%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%22$children%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22participating%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$rand%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$gt%22:%200.5%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22control%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$always%22:%20true%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)\n\nThe output will be the same as the [simple](#simple) example, but only if it is later than the January first 2020.\n\n### Missing children\n```json\n{\n    \"EXP001\": {\n        \"$children\": {\n          \"never\": { \"$always\":  false }\n        }\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22$children%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22never%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$always%22:%20false%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)\nThe output will be `EXP001` even if it has no active child.\n\n### And\n```json\n{\n    \"EXP001\": {\n        \"$date\": { \"$gt\": \"2020-01-01 15:00:00\" },\n        \"beta\": \"yes\"\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%0A%20%20%20%20%22beta%22:%20%22yes%22%0A%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22$date%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22$gt%22:%20%222020-01-01%2015:00:00%22%0A%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%22beta%22:%20%22yes%22%0A%20%20%20%20%7D%0A%7D)\nThe output will be `EXP001` if the context has a key `beta` as `yes` **and** is queried after January first 2020.\n\n\n### Exists\n```json\n{\n    \"EXP001\": {\n        \"beta\": {\n            \"$always\": true,\n            \"$not\": { \"$eq\": null }\n        }\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%0A%20%20%20%20%22beta%22:%20null%0A%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22beta%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22$always%22:%20true,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22$not%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22$eq%22:%20null%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)\nThe output will be `EXP001` if the context has a key `beta`, and it is not `null`.\n\n### Uncalled for\n```json\n{\n   \"EXP001\": {\n        \"$children\": {\n            \"control\": { \"$always\":  true },\n            \"participating\": { \"$rand\": { \"$gt\": 0.5 } }\n        }\n    }\n}\n```\n[Playground](https://koncierge-playground.herokuapp.com/?context=%7B%0A%20%20%20%20%22beta%22:%20%22yes%22%0A%7D\u0026experiment=%7B%0A%20%20%20%20%22EXP001%22:%20%7B%0A%20%20%20%20%20%20%20%20%22$date%22:%20%7B%20%22$gt%22:%20%222020-01-01%2015:00:00%22%20%7D,%0A%20%20%20%20%20%20%20%20%22beta%22:%20%22yes%22%0A%20%20%20%20%7D%0A%7D)\n\nThis will **always** output `EXP001.control`, as we parse children rules sequentially.\n\n### More examples\nYou can check out the `/test` folder for more examples and edge cases, as well as playing in the [Playground](https://koncierge-playground.herokuapp.com/)\n\n\n## Usage\nThe library is divided into two major [_namespaces_](https://en.wikipedia.org/wiki/Kan_extension).\n1. Parse _(Left Kan)_.\n1. Interpret _(Right Kan)_.\n\nA sub-section of the interpretation is also to validate that the given context can be matched to the rules.\n\n### Parser\nA function that takes the full JSON definition of the experiments and transforms it into objects that can be then [interpreted](#interpreter)\n\n### Interpreter\nA function that takes the [parsed](#parser) definition, the context provided, and a [`World`](https://en.wikipedia.org/wiki/Dependency_injection)\nand outputs the list of experiments (and variants) the context is part of\n\n### Context validation\nWe validate the context in an interpreter-agnostic way.\nThis means that even though semantically, some rules we might never call (see the [Uncalled for](#uncalled-for) example),\nthe context will still need to match every possible child rule.\n\n## Extras\n### Date\nBoth `$gt` and `$lt` only work with numbers, so dates will be compared by their epoch second.\nThe parser can interpret some date formats (read more about the date formats [here](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)).\nFormats with no timezone, we choose the default system timezone. This is not recommended.\n\n| Format | Example | Notes |\n|--------|---------|-----|\n| `yyyy-MM-dd` | 2020-10-05 | The time is at 00:00 from the system's timezone |\n| `EEE MMM dd yyyy HH:mm:ss 'GMT'Z (z)` | Tue Oct 06 2020 00:30:00 GMT+0200 (Central European Summer Time) | JavaScript's default `Date` format |\n| `yyyy-MM-dd'T'HH:mm:ss.SSSZ` | 2020-10-05T22:20:00.000+0200 | **Recommended** [ISO 8601-2:2019](https://www.iso.org/standard/70908.html) standard |\n\n### Randomness\nEven though `$rand` and `$chaos` might look similar; they differ on the seed for its randomness.\nWith `$rand`, any random value generated with the same context and variant name, will be the same output.\n\nFor this reason, most of the times you'll want to narrow down the context before applying `$rand`.\nFor example, given this context:\n```json\n{\n    \"user_id\": 3,\n    \"last_login\": \"2020-10-15T10:00:00.000Z\"\n}\n```\nthe `last_login` will probably change throughout the time, but we need the experiment to be fixed by the `user_id`.\nIn such a scenario, we can write the experiment as such:\n```json\n{\n    \"EXP001\": {\n        \"user_id\": {\n            \"$rand\": { \"$gt\": 0.5 }\n        }\n    }\n}\n``` \n\nEvery time the `user_id: 3` queries the experiment, the `$rand` value will be the same.\n\nIn contrast, `$chaos` will generate a new value each time, so there is no guarantee in what variant `user_id: 3` will fall.\n\n#### Rolling experiment updates\nAs we generate the same number for each context, you can safely update the threshold and rest assured that the minimum\nnumber of clients will change variants.\n\n### TODO\n1. Escape the `$` to be able to match to `$` keys, and the `.` in keys to match not-nested keys with `.`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjazcarate%2Fkoncierge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjazcarate%2Fkoncierge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjazcarate%2Fkoncierge/lists"}