Conversation
Core SDK with context management, variant assignment, audience matching, JSON expression evaluation, and comprehensive test suite.
WalkthroughThis PR adds a complete C++ SDK for A/B Smartly: a CMake build (C++17) with FetchContent for nlohmann::json and optional CURL, a public library target Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (17)
tests/CMakeLists.txt (1)
1-4: AddURL_HASHto FetchContent_Declare for integrity verification.Adding a SHA-256 hash ensures reproducible fetches and guards against tampering. The hash value for the Catch2 v3.5.2 tarball should be computed and added:
🔒 Suggested change
FetchContent_Declare( Catch2 URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.5.2.tar.gz + URL_HASH SHA256=<compute via: curl -sL "https://github.com/catchorg/Catch2/archive/refs/tags/v3.5.2.tar.gz" | sha256sum> )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/CMakeLists.txt` around lines 1 - 4, The FetchContent_Declare block for Catch2 (the call to FetchContent_Declare with the Catch2 target and its URL) is missing an integrity hash; compute the SHA-256 of the v3.5.2 tarball and add a URL_HASH entry to that FetchContent_Declare (use the CMake form URL_HASH SHA256=<computed-hash>) so the Catch2 declaration includes the integrity check.CMakeLists.txt (1)
9-12: Add an integrity hash for the fetched nlohmann_json archive.Using
URL_HASHhelps prevent supply‑chain drift if the tarball changes. Compute the SHA‑256 for the release artefact and pin it here.🔐 Suggested pinning
FetchContent_Declare( json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH SHA256=<sha256> )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CMakeLists.txt` around lines 9 - 12, The FetchContent_Declare block for the dependency named "json" currently fetches the release tarball without integrity verification; compute the SHA-256 of the v3.11.3 json.tar.xz release artifact and add a URL_HASH entry to the FetchContent_Declare for the "json" target (i.e., the FetchContent_Declare(...) that references json and URL) using the format expected by CMake so the fetch is pinned to that SHA-256 digest.src/utils.cpp (1)
55-64: Make empty split handling explicit.Line 63 currently relies on
split.size() - 1, which underflows to ‑1 when the split is empty. An explicit guard clarifies the contract and avoids accidental indexing by callers.♻️ Suggested guard
int choose_variant(const std::vector<double>& split, double probability) { + if (split.empty()) { + return -1; + } double cum_sum = 0.0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils.cpp` around lines 55 - 64, The function choose_variant currently underflows when split is empty; add an explicit guard at the start of choose_variant that checks split.empty() and fails fast (e.g., throw std::invalid_argument("choose_variant: split must not be empty")) so callers can't accidentally rely on split.size()-1; update error message to include the function name and ensure the header <stdexcept> is included where appropriate.src/context.cpp (5)
14-20:array_equals_shallowis equivalent tostd::vector::operator==.
std::vector<double>::operator==already performs element-wise comparison. This helper can be replaced bya == b.♻️ Proposed simplification
-static bool array_equals_shallow(const std::vector<double>& a, const std::vector<double>& b) { - if (a.size() != b.size()) return false; - for (size_t i = 0; i < a.size(); ++i) { - if (a[i] != b[i]) return false; - } - return true; -}Then at the call site (line 654):
- && array_equals_shallow(experiment.trafficSplit, assignment.traffic_split); + && experiment.trafficSplit == assignment.traffic_split;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 14 - 20, The helper function array_equals_shallow duplicates std::vector<double>::operator==; remove the array_equals_shallow definition and replace every call site (e.g., occurrences where array_equals_shallow(a,b) is used) with the direct comparison (a == b) using the original vector variables; ensure headers remain for std::vector and that no other code expects a non-boolean behavior from array_equals_shallow before deleting it.
40-41: Redundantdata_re-assignment ininit().The constructor already initialises
data_via the member initialiser list (line 25:data_(std::move(data))), then callsinit(data_)which re-assignsdata_ = data(line 575). This copiesdata_back onto itself — harmless but wasteful. Consider havinginitonly callbuild_index(), or removing the redundant assignment.♻️ Proposed fix
void Context::init(const ContextData& data) { - data_ = data; build_index(); }Or inline the call in the constructor:
- init(data_); + build_index();Also applies to: 574-577
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 40 - 41, The constructor sets data_ via the member initializer list then immediately calls init(data_) which reassigns data_ to itself; remove the redundant assignment by changing init to not reassign data_ (i.e., drop `data_ = data` inside init) and have init only call build_index(), or alternatively change the constructor to call build_index() directly instead of init(data_); update references to init() and the constructor accordingly (symbols: init, build_index, data_).
309-337:track()only stores numeric properties inGoalAchievementbut emits all properties in the event.Lines 318–321 filter
goal.propertiesto numeric values only, but line 330 emits the originalpropertiesJSON (including non-numeric values) in the event payload. If this is intentional (event handler sees everything, but the publish payload is filtered), it's worth a brief comment to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 309 - 337, Context::track currently stores only numeric entries into GoalAchievement::properties but emits the original properties JSON to emit_event, causing a mismatch; change the emission to reflect the filtered stored properties by converting goal.properties into a nlohmann::json (or null when empty) and assign that to goal_data["properties"] before calling emit_event("goal", goal_data) so the event payload matches the stored GoalAchievement, referencing Context::track, GoalAchievement::properties, and emit_event.
412-540:get_or_create_assignment— correct but dense; consider extracting sub-functions.This 130-line method handles cache invalidation, overrides, audience matching, traffic eligibility, custom assignments, full-on variants, and variable resolution. Breaking it into smaller helpers (e.g.,
compute_assignment_for_experiment,apply_override) would improve readability and testability without changing behaviour.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 412 - 540, The get_or_create_assignment function is too large and should be split into smaller helpers to improve readability and testability; refactor by extracting the distinct responsibilities into methods such as apply_override(Assignment&, const ExperimentIndex*, const std::string& experiment_name), evaluate_audience(const ExperimentData&, Assignment&), determine_variant_and_eligibility(const ExperimentData&, Assignment&, const std::string& unit_type), and resolve_variables_if_applicable(Assignment&, const ExperimentIndex*), then rewrite get_or_create_assignment to perform high-level flow: check cache/overrides, call apply_override when overrides_ exists, otherwise call evaluate_audience then determine_variant_and_eligibility, set meta fields (id, iteration, traffic_split, attrs_seq), call resolve_variables_if_applicable, and finally store/move the Assignment into assignments_ so behavior remains identical but logic is modularized (use existing symbols: get_or_create_assignment, assignments_, overrides_, index_, audience_matcher_, assigners_, units_, cassignments_).
616-646: Manual sorted insertion inbuild_indexcould be simplified.The nested loop with manual insertion-point search for
index_variables_can be replaced withstd::lower_boundfor clarity and to avoid the duplicate-check logic interleaved with insertion.♻️ Simplified with std::lower_bound
- if (!found) { - bool inserted = false; - for (auto it = vec.begin(); it != vec.end(); ++it) { - if (entry.data->id < (*it)->data->id) { - vec.insert(it, &entry); - inserted = true; - break; - } - if (entry.data->id == (*it)->data->id) { - inserted = true; - break; - } - } - if (!inserted) { - vec.push_back(&entry); - } - } + if (!found) { + auto pos = std::lower_bound( + vec.begin(), vec.end(), &entry, + [](const ExperimentIndex* a, const ExperimentIndex* b) { + return a->data->id < b->data->id; + }); + if (pos == vec.end() || (*pos)->data->id != entry.data->id) { + vec.insert(pos, &entry); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 616 - 646, The manual sorted insertion in build_index that walks vec to insert &entry (using index_variables_, index_, and entry) should be replaced with std::lower_bound to find the insertion position and a single duplicate check; for each key get auto& vec = index_variables_[key], use std::lower_bound(vec.begin(), vec.end(), &entry, [](const ContextEntry* a, const ContextEntry* b){ return a->data->id < b->data->id; }) to obtain the iterator, then if the iterator is at end or (*it)->data->id != entry.data->id (or pointer inequality) call vec.insert(it, &entry); this removes the manual loop/flags and requires adding `#include` <algorithm> and using the existing symbols index_variables_, entry, index_, and build_index.src/json_expr/evaluator.cpp (3)
4-4: Unused include<charconv>.This header is included but never used — the implementation uses
std::stodandstd::ostringstreaminstead ofstd::from_chars/std::to_chars.🧹 Remove unused include
`#include` <cmath> -#include <charconv> `#include` <sstream>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/json_expr/evaluator.cpp` at line 4, Remove the unused `#include` <charconv> from src/json_expr/evaluator.cpp: the implementation uses std::stod and std::ostringstream (see functions around evaluator.cpp where std::stod and std::ostringstream are used), so delete the <charconv> include to avoid an unnecessary dependency and compilation warning.
35-43: Object expression dispatch relies on JSON key iteration order.The loop iterates the object's keys, checks only the first key against the operator map, and immediately
breaks. This is intentional for the single-key expression format, butnlohmann::jsonobjects iterate in alphabetical order (they usestd::mapinternally by default). This means{"gt": [...], "value": 5}would match"gt"rather than"value", which is fine — but it's worth a comment to make the assumption explicit for maintainers.📝 Suggested inline comment
if (expr.is_object()) { + // Expression objects must contain exactly one operator key. + // We check only the first key (alphabetical order in nlohmann::json). for (auto it = expr.begin(); it != expr.end(); ++it) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/json_expr/evaluator.cpp` around lines 35 - 43, The object-dispatch in Evaluator::evaluate relies on iterating a nlohmann::json object and using only the first key to find an operator in operators_, which depends on nlohmann::json's alphabetical iteration order and the convention that expressions are single-key objects; update the evaluate function to include an explicit inline comment documenting that assumption (mention nlohmann::json uses std::map and iterates keys alphabetically), and optionally add a small runtime check (e.g., assert or throw) that the object has exactly one key before using the first key to select the operator to make the contract explicit; reference the evaluate method and the operators_ lookup in your change.
139-148: Direct floating-point equality comparison incompare.
lv == rvat line 144 uses directdoubleequality. For the intended use case (small integers and simple decimals in A/B testing config), this is acceptable. Just flagging that an epsilon-based comparison would be more robust if fractional arithmetic ever feeds into comparison operands.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/json_expr/evaluator.cpp` around lines 139 - 148, The compare logic in function compare uses direct double equality (lv == rv) which is fragile; change it to an epsilon-based comparison: compute if (std::fabs(lv - rv) < EPS) treat as equal, else return lv > rv ? 1 : -1; define a small constant EPS (e.g. 1e-9) scoped to the file or function and include <cmath> or use std::abs, and update the branch in evaluator.cpp where lv and rv are compared to use this epsilon comparison.src/json_expr/operators.cpp (2)
243-275:std::regexis compiled on every evaluation — acceptable but worth noting.
std::regexconstruction is expensive. IfMatchOperatoris evaluated repeatedly with the same pattern (e.g., during bulk audience matching), this could become a hot path. Not critical for typical A/B testing workloads, but a regex cache could be added later if profiling shows contention.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/json_expr/operators.cpp` around lines 243 - 275, MatchOperator::evaluate currently constructs std::regex every call which is expensive; add a simple cache of compiled regex objects keyed by the pattern string in the MatchOperator class (e.g., an std::unordered_map<std::string, std::regex> member) and lookup/insert before constructing a new std::regex inside MatchOperator::evaluate; protect the map with a mutex or shared_mutex for thread-safety when reading/writing and reuse the cached std::regex for std::regex_search, falling back to compiling and caching on regex construction errors (same error handling as today).
104-194: Significant code duplication acrossGt,Gte,Lt,Lteoperators.These four operators share identical boilerplate (array check, lhs/rhs evaluation, null guards, compare call) and differ only in the comparison predicate. Extracting a shared helper would reduce ~90 lines to ~20 and make adding new comparison operators trivial.
♻️ Proposed helper-based refactor
+// In a shared location (e.g., anonymous namespace in operators.cpp): +static nlohmann::json compare_op( + const Evaluator& evaluator, + const nlohmann::json& args, + const nlohmann::json& vars, + std::function<bool(int)> predicate +) { + if (!args.is_array()) return nullptr; + auto lhs = args.size() > 0 ? evaluator.evaluate(args[0], vars) : nlohmann::json(nullptr); + if (lhs.is_null()) return nullptr; + auto rhs = args.size() > 1 ? evaluator.evaluate(args[1], vars) : nlohmann::json(nullptr); + if (rhs.is_null()) return nullptr; + auto result = Evaluator::compare(lhs, rhs); + if (result.has_value()) return predicate(*result); + return nullptr; +} + nlohmann::json GtOperator::evaluate( const Evaluator& evaluator, const nlohmann::json& args, const nlohmann::json& vars ) const { - if (!args.is_array()) { - return nullptr; - } - auto lhs = args.size() > 0 ? evaluator.evaluate(args[0], vars) : nlohmann::json(nullptr); - if (lhs.is_null()) { - return nullptr; - } - auto rhs = args.size() > 1 ? evaluator.evaluate(args[1], vars) : nlohmann::json(nullptr); - if (rhs.is_null()) { - return nullptr; - } - auto result = Evaluator::compare(lhs, rhs); - if (result.has_value()) { - return *result > 0; - } - return nullptr; + return compare_op(evaluator, args, vars, [](int r) { return r > 0; }); }Apply similarly to
GteOperator,LtOperator, andLteOperator.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/json_expr/operators.cpp` around lines 104 - 194, The four evaluate methods (GtOperator::evaluate, GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) duplicate the same argument checking, lhs/rhs evaluation, null guards and call to Evaluator::compare; extract a shared helper (e.g., evaluateComparison or evaluateBinaryComparison) that takes (const Evaluator& evaluator, const nlohmann::json& args, const nlohmann::json& vars, std::function<bool(int)> predicate) and encapsulates: array check, evaluate lhs/rhs, null checks, call Evaluator::compare and return predicate(result) as nlohmann::json or nullptr on failure; then rewrite each operator (GtOperator::evaluate, GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) to call this helper with the appropriate lambda (r>0, r>=0, r<0, r<=0).include/absmartly/context.h (2)
16-16:<chrono>is unused in this header.
std::chronois used only incontext.cpp(vianow_millis). Moving it to the implementation file reduces transitive include costs.🧹 Move to implementation file
-#include <chrono>And ensure
#include <chrono>is present insrc/context.cpp(it already is at line 5).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@include/absmartly/context.h` at line 16, Remove the unused header include <chrono> from include/absmartly/context.h because std::chrono is only used in the implementation (now_millis in context.cpp); ensure `#include` <chrono> remains (or is added) in src/context.cpp so the implementation compiles, and run a quick build to confirm no transitive include breakage.
20-42:AssignmentandExperimentIndexare implementation details leaked into the public header.These structs are used internally by
Context(private members) and are not part of the public API surface. Exposing them in the header couples consumers to internal representation. Consider forward-declaring them and defining them in an internal header or incontext.cpp.This is fine for an initial implementation and can be deferred.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@include/absmartly/context.h` around lines 20 - 42, The public header is leaking internal implementation details by defining struct Assignment and struct ExperimentIndex; instead, forward-declare these types in the public header and move their full definitions into an internal header or context.cpp. Update Context's private members to use opaque pointers (e.g., std::unique_ptr<Assignment>/ExperimentIndex or raw pointers) so the compiler only needs the forward declarations in include/absmartly/context.h, and add the moved definitions and any required includes (nlohmann::json, vector, map, ExperimentData) to the internal header or context.cpp where Context's methods are implemented.include/absmartly/json_expr/operators.h (1)
3-6: Remove<regex>from the header — it is only used in the.cppimplementation.
<regex>is a notoriously heavy header and is only needed byMatchOperator::evaluateinoperators.cpp. Including it here inflates compile times for every translation unit that includesoperators.h.🧹 Proposed fix
`#include` <memory> `#include` <optional> -#include <regex> `#include` <string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@include/absmartly/json_expr/operators.h` around lines 3 - 6, The header operators.h unnecessarily includes <regex>, which increases compile times; remove the `#include` <regex> from operators.h and ensure the implementation file (operators.cpp) includes <regex> so MatchOperator::evaluate has the needed declaration; keep other headers (<memory>, <optional>, <string>) in operators.h and verify no other declarations in the header depend on std::regex.tests/json_expr_test.cpp (1)
332-416: Good integration tests — consider adding edge-case coverage fornulloperator andin/matchoperators.The integration tests effectively validate composite audience-matching expressions. You may want to add integration-level tests for the
null,in, andmatchoperators as well, since they are registered in theEvaluatorbut not exercised here (they may be tested inoperators_test.cpp, but integration-level coverage would strengthen confidence).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/json_expr_test.cpp` around lines 332 - 416, Add integration tests that exercise the registered 'null', 'in', and 'match' operators by creating new SECTIONs that call Evaluator::evaluate_boolean with appropriate json expressions built using the existing helpers (value_for, var_for, binary_op, unary_op) and the same sample subjects (john, terry, kate, maria); for 'null' construct an expression that checks a missing or explicitly null field, for 'in' use binary_op("in", var_for("language"), value_for(json::array({...}))) to assert membership, and for 'match' use binary_op("match", var_for("language"), value_for("regex-or-pattern")) to validate pattern matching — reuse the existing test structure and REQUIRE assertions similar to other SECTIONs to verify expected boolean results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@include/absmartly/models.h`:
- Around line 42-47: The from_json deserializer for CustomFieldValue can leave
stale data in CustomFieldValue::value when the JSON "value" key is missing or
null; update inline void from_json(const nlohmann::json& j, CustomFieldValue& v)
to explicitly reset v.value to std::nullopt in the else path (i.e., when
!j.contains("value") or j.at("value").is_null()), leaving the existing get logic
when a non-null value is present so existing instances get cleared correctly.
In `@README.md`:
- Around line 11-37: The Installation section in README.md uses H2 then jumps to
H4 (the "#### CMake (FetchContent)", "#### CMake (Local)", and "#### Building
from Source" headings); update those subsection headings to H3 (change #### to
###) so heading levels increment properly and satisfy markdownlint's
heading-increment rule—ensure all related subsection headings under the
Installation H2 are consistently H3.
- Around line 185-187: Add a comma after "logs in" in the sentence describing
when to call set_unit() or set_units() to improve readability; update the README
sentence mentioning set_unit(), set_units() and context so it reads "...when a
user logs in, and you want to associate..." (keep references to set_unit(),
set_units(), and context unchanged).
In `@src/context.cpp`:
- Around line 273-278: The code in custom_field_value uses std::stod (in the
block handling field.type == "number"), which is locale-dependent; replace the
std::stod call with a locale-independent parser such as std::from_chars (include
<charconv>), parse val into a double using std::from_chars, check the returned
error code (std::errc or std::from_chars_result) and return nullptr on failure
(same behavior as the current catch path), and remove the try/catch; update the
custom_field_value function to handle the from_chars result and preserve the
existing return semantics.
- Around line 153-155: set_override currently mutates overrides_ after
finalisation while set_custom_assignment enforces immutability via
check_not_finalized(); call check_not_finalized() at the start of both
Context::set_override and Context::set_overrides to prevent post-finalisation
mutation and keep behavior consistent with set_custom_assignment (or, if
overrides are intentionally mutable, add explicit documentation to that effect
instead). Ensure you reference the check_not_finalized() guard and the
overrides_ mutations in these methods so tests expecting post-finalisation
mutation to be disallowed will pass.
In `@src/json_expr/evaluator.cpp`:
- Around line 112-124: The number-to-string path in src/json_expr/evaluator.cpp
uses std::ostringstream (the two local variables named oss) which pick up the
global locale; imbue each ostringstream with the C locale (call
oss.imbue(std::locale::classic()) immediately after construction) before
formatting so decimal separators are always '.'; also ensure <locale> is
included if not already.
- Around line 84-100: The string-to-double conversion uses locale-sensitive
std::stod; replace it with locale-independent std::from_chars (include
<charconv>) to parse the std::string 'str' (or a std::string_view) into a double
named 'result', remove the try/catch and the pos variable, then check that
from_chars returned no error and consumed the entire input (the end pointer
equals str.data()+str.size()) and that std::isfinite(result) before returning
result; otherwise return std::nullopt — update the conversion logic in the block
that currently references value, str, pos, result to use std::from_chars
instead.
In `@src/md5.cpp`:
- Around line 169-174: The code currently writes only a 32-bit length into
block[14], truncating inputs ≥512MB; compute the full 64-bit bit-length (e.g.
uint64_t bitlen = static_cast<uint64_t>(len) << 3), then store the low 32 bits
into block[14] and the high 32 bits into block[15] before calling
md5cycle(state, block) so the MD5 length is encoded as a 64-bit value.
---
Nitpick comments:
In `@CMakeLists.txt`:
- Around line 9-12: The FetchContent_Declare block for the dependency named
"json" currently fetches the release tarball without integrity verification;
compute the SHA-256 of the v3.11.3 json.tar.xz release artifact and add a
URL_HASH entry to the FetchContent_Declare for the "json" target (i.e., the
FetchContent_Declare(...) that references json and URL) using the format
expected by CMake so the fetch is pinned to that SHA-256 digest.
In `@include/absmartly/context.h`:
- Line 16: Remove the unused header include <chrono> from
include/absmartly/context.h because std::chrono is only used in the
implementation (now_millis in context.cpp); ensure `#include` <chrono> remains (or
is added) in src/context.cpp so the implementation compiles, and run a quick
build to confirm no transitive include breakage.
- Around line 20-42: The public header is leaking internal implementation
details by defining struct Assignment and struct ExperimentIndex; instead,
forward-declare these types in the public header and move their full definitions
into an internal header or context.cpp. Update Context's private members to use
opaque pointers (e.g., std::unique_ptr<Assignment>/ExperimentIndex or raw
pointers) so the compiler only needs the forward declarations in
include/absmartly/context.h, and add the moved definitions and any required
includes (nlohmann::json, vector, map, ExperimentData) to the internal header or
context.cpp where Context's methods are implemented.
In `@include/absmartly/json_expr/operators.h`:
- Around line 3-6: The header operators.h unnecessarily includes <regex>, which
increases compile times; remove the `#include` <regex> from operators.h and ensure
the implementation file (operators.cpp) includes <regex> so
MatchOperator::evaluate has the needed declaration; keep other headers
(<memory>, <optional>, <string>) in operators.h and verify no other declarations
in the header depend on std::regex.
In `@src/context.cpp`:
- Around line 14-20: The helper function array_equals_shallow duplicates
std::vector<double>::operator==; remove the array_equals_shallow definition and
replace every call site (e.g., occurrences where array_equals_shallow(a,b) is
used) with the direct comparison (a == b) using the original vector variables;
ensure headers remain for std::vector and that no other code expects a
non-boolean behavior from array_equals_shallow before deleting it.
- Around line 40-41: The constructor sets data_ via the member initializer list
then immediately calls init(data_) which reassigns data_ to itself; remove the
redundant assignment by changing init to not reassign data_ (i.e., drop `data_ =
data` inside init) and have init only call build_index(), or alternatively
change the constructor to call build_index() directly instead of init(data_);
update references to init() and the constructor accordingly (symbols: init,
build_index, data_).
- Around line 309-337: Context::track currently stores only numeric entries into
GoalAchievement::properties but emits the original properties JSON to
emit_event, causing a mismatch; change the emission to reflect the filtered
stored properties by converting goal.properties into a nlohmann::json (or null
when empty) and assign that to goal_data["properties"] before calling
emit_event("goal", goal_data) so the event payload matches the stored
GoalAchievement, referencing Context::track, GoalAchievement::properties, and
emit_event.
- Around line 412-540: The get_or_create_assignment function is too large and
should be split into smaller helpers to improve readability and testability;
refactor by extracting the distinct responsibilities into methods such as
apply_override(Assignment&, const ExperimentIndex*, const std::string&
experiment_name), evaluate_audience(const ExperimentData&, Assignment&),
determine_variant_and_eligibility(const ExperimentData&, Assignment&, const
std::string& unit_type), and resolve_variables_if_applicable(Assignment&, const
ExperimentIndex*), then rewrite get_or_create_assignment to perform high-level
flow: check cache/overrides, call apply_override when overrides_ exists,
otherwise call evaluate_audience then determine_variant_and_eligibility, set
meta fields (id, iteration, traffic_split, attrs_seq), call
resolve_variables_if_applicable, and finally store/move the Assignment into
assignments_ so behavior remains identical but logic is modularized (use
existing symbols: get_or_create_assignment, assignments_, overrides_, index_,
audience_matcher_, assigners_, units_, cassignments_).
- Around line 616-646: The manual sorted insertion in build_index that walks vec
to insert &entry (using index_variables_, index_, and entry) should be replaced
with std::lower_bound to find the insertion position and a single duplicate
check; for each key get auto& vec = index_variables_[key], use
std::lower_bound(vec.begin(), vec.end(), &entry, [](const ContextEntry* a, const
ContextEntry* b){ return a->data->id < b->data->id; }) to obtain the iterator,
then if the iterator is at end or (*it)->data->id != entry.data->id (or pointer
inequality) call vec.insert(it, &entry); this removes the manual loop/flags and
requires adding `#include` <algorithm> and using the existing symbols
index_variables_, entry, index_, and build_index.
In `@src/json_expr/evaluator.cpp`:
- Line 4: Remove the unused `#include` <charconv> from
src/json_expr/evaluator.cpp: the implementation uses std::stod and
std::ostringstream (see functions around evaluator.cpp where std::stod and
std::ostringstream are used), so delete the <charconv> include to avoid an
unnecessary dependency and compilation warning.
- Around line 35-43: The object-dispatch in Evaluator::evaluate relies on
iterating a nlohmann::json object and using only the first key to find an
operator in operators_, which depends on nlohmann::json's alphabetical iteration
order and the convention that expressions are single-key objects; update the
evaluate function to include an explicit inline comment documenting that
assumption (mention nlohmann::json uses std::map and iterates keys
alphabetically), and optionally add a small runtime check (e.g., assert or
throw) that the object has exactly one key before using the first key to select
the operator to make the contract explicit; reference the evaluate method and
the operators_ lookup in your change.
- Around line 139-148: The compare logic in function compare uses direct double
equality (lv == rv) which is fragile; change it to an epsilon-based comparison:
compute if (std::fabs(lv - rv) < EPS) treat as equal, else return lv > rv ? 1 :
-1; define a small constant EPS (e.g. 1e-9) scoped to the file or function and
include <cmath> or use std::abs, and update the branch in evaluator.cpp where lv
and rv are compared to use this epsilon comparison.
In `@src/json_expr/operators.cpp`:
- Around line 243-275: MatchOperator::evaluate currently constructs std::regex
every call which is expensive; add a simple cache of compiled regex objects
keyed by the pattern string in the MatchOperator class (e.g., an
std::unordered_map<std::string, std::regex> member) and lookup/insert before
constructing a new std::regex inside MatchOperator::evaluate; protect the map
with a mutex or shared_mutex for thread-safety when reading/writing and reuse
the cached std::regex for std::regex_search, falling back to compiling and
caching on regex construction errors (same error handling as today).
- Around line 104-194: The four evaluate methods (GtOperator::evaluate,
GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) duplicate
the same argument checking, lhs/rhs evaluation, null guards and call to
Evaluator::compare; extract a shared helper (e.g., evaluateComparison or
evaluateBinaryComparison) that takes (const Evaluator& evaluator, const
nlohmann::json& args, const nlohmann::json& vars, std::function<bool(int)>
predicate) and encapsulates: array check, evaluate lhs/rhs, null checks, call
Evaluator::compare and return predicate(result) as nlohmann::json or nullptr on
failure; then rewrite each operator (GtOperator::evaluate,
GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) to call this
helper with the appropriate lambda (r>0, r>=0, r<0, r<=0).
In `@src/utils.cpp`:
- Around line 55-64: The function choose_variant currently underflows when split
is empty; add an explicit guard at the start of choose_variant that checks
split.empty() and fails fast (e.g., throw std::invalid_argument("choose_variant:
split must not be empty")) so callers can't accidentally rely on split.size()-1;
update error message to include the function name and ensure the header
<stdexcept> is included where appropriate.
In `@tests/CMakeLists.txt`:
- Around line 1-4: The FetchContent_Declare block for Catch2 (the call to
FetchContent_Declare with the Catch2 target and its URL) is missing an integrity
hash; compute the SHA-256 of the v3.5.2 tarball and add a URL_HASH entry to that
FetchContent_Declare (use the CMake form URL_HASH SHA256=<computed-hash>) so the
Catch2 declaration includes the integrity check.
In `@tests/json_expr_test.cpp`:
- Around line 332-416: Add integration tests that exercise the registered
'null', 'in', and 'match' operators by creating new SECTIONs that call
Evaluator::evaluate_boolean with appropriate json expressions built using the
existing helpers (value_for, var_for, binary_op, unary_op) and the same sample
subjects (john, terry, kate, maria); for 'null' construct an expression that
checks a missing or explicitly null field, for 'in' use binary_op("in",
var_for("language"), value_for(json::array({...}))) to assert membership, and
for 'match' use binary_op("match", var_for("language"),
value_for("regex-or-pattern")) to validate pattern matching — reuse the existing
test structure and REQUIRE assertions similar to other SECTIONs to verify
expected boolean results.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
CMakeLists.txtREADME.mdinclude/absmartly/audience_matcher.hinclude/absmartly/context.hinclude/absmartly/context_config.hinclude/absmartly/context_event_handler.hinclude/absmartly/errors.hinclude/absmartly/hashing.hinclude/absmartly/json_expr/evaluator.hinclude/absmartly/json_expr/operators.hinclude/absmartly/models.hinclude/absmartly/sdk.hinclude/absmartly/variant_assigner.hsrc/audience_matcher.cppsrc/context.cppsrc/json_expr/evaluator.cppsrc/json_expr/operators.cppsrc/md5.cppsrc/murmur3.cppsrc/utils.cppsrc/variant_assigner.cpptests/CMakeLists.txttests/audience_matcher_test.cpptests/context_test.cpptests/json_expr_test.cpptests/md5_test.cpptests/murmur3_test.cpptests/operators_test.cpptests/utils_test.cpptests/variant_assigner_test.cpp
| inline void from_json(const nlohmann::json& j, CustomFieldValue& v) { | ||
| j.at("name").get_to(v.name); | ||
| j.at("type").get_to(v.type); | ||
| if (j.contains("value") && !j.at("value").is_null()) { | ||
| v.value = j.at("value").get<std::string>(); | ||
| } |
There was a problem hiding this comment.
Reset CustomFieldValue::value when JSON is null or missing.
If from_json is called on an existing instance, a null/missing value leaves stale data in v.value. Resetting to std::nullopt avoids that.
🛠️ Suggested fix
inline void from_json(const nlohmann::json& j, CustomFieldValue& v) {
j.at("name").get_to(v.name);
j.at("type").get_to(v.type);
if (j.contains("value") && !j.at("value").is_null()) {
v.value = j.at("value").get<std::string>();
+ } else {
+ v.value = std::nullopt;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| inline void from_json(const nlohmann::json& j, CustomFieldValue& v) { | |
| j.at("name").get_to(v.name); | |
| j.at("type").get_to(v.type); | |
| if (j.contains("value") && !j.at("value").is_null()) { | |
| v.value = j.at("value").get<std::string>(); | |
| } | |
| inline void from_json(const nlohmann::json& j, CustomFieldValue& v) { | |
| j.at("name").get_to(v.name); | |
| j.at("type").get_to(v.type); | |
| if (j.contains("value") && !j.at("value").is_null()) { | |
| v.value = j.at("value").get<std::string>(); | |
| } else { | |
| v.value = std::nullopt; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@include/absmartly/models.h` around lines 42 - 47, The from_json deserializer
for CustomFieldValue can leave stale data in CustomFieldValue::value when the
JSON "value" key is missing or null; update inline void from_json(const
nlohmann::json& j, CustomFieldValue& v) to explicitly reset v.value to
std::nullopt in the else path (i.e., when !j.contains("value") or
j.at("value").is_null()), leaving the existing get logic when a non-null value
is present so existing instances get cleared correctly.
| #### CMake (FetchContent) | ||
|
|
||
| Add the following to your `CMakeLists.txt` to include the SDK directly from the repository: | ||
|
|
||
| ```cmake | ||
| include(FetchContent) | ||
|
|
||
| FetchContent_Declare( | ||
| absmartly-sdk | ||
| GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git | ||
| GIT_TAG main | ||
| ) | ||
| FetchContent_MakeAvailable(absmartly-sdk) | ||
|
|
||
| target_link_libraries(your_target PRIVATE absmartly-sdk) | ||
| ``` | ||
|
|
||
| #### CMake (Local) | ||
|
|
||
| If you have cloned the repository locally, you can add it as a subdirectory: | ||
|
|
||
| ```cmake | ||
| add_subdirectory(path/to/cpp-sdk) | ||
| target_link_libraries(your_target PRIVATE absmartly-sdk) | ||
| ``` | ||
|
|
||
| #### Building from Source |
There was a problem hiding this comment.
Fix heading level jumps in the Installation section.
Line 11 (and the following subsection headings) jump from H2 to H4, which triggers markdownlint’s heading increment rule. Consider using H3 for these subsections.
Proposed change
-#### CMake (FetchContent)
+### CMake (FetchContent)
@@
-#### CMake (Local)
+### CMake (Local)
@@
-#### Building from Source
+### Building from Source📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #### CMake (FetchContent) | |
| Add the following to your `CMakeLists.txt` to include the SDK directly from the repository: | |
| ```cmake | |
| include(FetchContent) | |
| FetchContent_Declare( | |
| absmartly-sdk | |
| GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git | |
| GIT_TAG main | |
| ) | |
| FetchContent_MakeAvailable(absmartly-sdk) | |
| target_link_libraries(your_target PRIVATE absmartly-sdk) | |
| ``` | |
| #### CMake (Local) | |
| If you have cloned the repository locally, you can add it as a subdirectory: | |
| ```cmake | |
| add_subdirectory(path/to/cpp-sdk) | |
| target_link_libraries(your_target PRIVATE absmartly-sdk) | |
| ``` | |
| #### Building from Source | |
| ### CMake (FetchContent) | |
| Add the following to your `CMakeLists.txt` to include the SDK directly from the repository: | |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 11-11: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4
(MD001, heading-increment)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 11 - 37, The Installation section in README.md uses
H2 then jumps to H4 (the "#### CMake (FetchContent)", "#### CMake (Local)", and
"#### Building from Source" headings); update those subsection headings to H3
(change #### to ###) so heading levels increment properly and satisfy
markdownlint's heading-increment rule—ensure all related subsection headings
under the Installation H2 are consistently H3.
| ### Setting Extra Units | ||
|
|
||
| You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead. |
There was a problem hiding this comment.
Minor punctuation tweak for readability.
Line 187 reads more smoothly with a comma after “logs in”.
Proposed change
-You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context.
+You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Setting Extra Units | |
| You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead. | |
| ### Setting Extra Units | |
| You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead. |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~187-~187: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...hods. This is useful when a user logs in and you want to associate the new identity ...
(COMMA_COMPOUND_SENTENCE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 185 - 187, Add a comma after "logs in" in the
sentence describing when to call set_unit() or set_units() to improve
readability; update the README sentence mentioning set_unit(), set_units() and
context so it reads "...when a user logs in, and you want to associate..." (keep
references to set_unit(), set_units(), and context unchanged).
| void Context::set_override(const std::string& experiment_name, int variant) { | ||
| overrides_[experiment_name] = variant; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/context.cpp | sed -n '145,175p'Repository: absmartly/cpp-sdk
Length of output: 1248
🏁 Script executed:
rg -n "check_not_finalized" --type=cpp -B2 -A2Repository: absmartly/cpp-sdk
Length of output: 2885
🏁 Script executed:
rg -n "set_override|set_custom_assignment" --type=cpp -B3 -A5Repository: absmartly/cpp-sdk
Length of output: 11180
🏁 Script executed:
find . -name "*test*.cpp" -o -name "*test*.h" | head -20Repository: absmartly/cpp-sdk
Length of output: 276
🏁 Script executed:
rg -n "override|finalize" include/absmartly/context.h -B2 -A2Repository: absmartly/cpp-sdk
Length of output: 1187
🏁 Script executed:
cat -n src/context.cpp | sed -n '700,710p'Repository: absmartly/cpp-sdk
Length of output: 426
🏁 Script executed:
rg -n "should throw after finalize" tests/context_test.cpp -B5 -A10Repository: absmartly/cpp-sdk
Length of output: 3843
set_override allows state mutation on finalised contexts — inconsistent with set_custom_assignment.
set_custom_assignment (line 164) calls check_not_finalized() to prevent mutations after finalisation, but set_override does not. The test suite (line 837–841) explicitly verifies that set_custom_assignment throws after finalisation, yet contains no corresponding test for set_override. If overrides are intentionally permitted post-finalisation, this should be documented; otherwise, add check_not_finalized() to set_override and set_overrides for consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.cpp` around lines 153 - 155, set_override currently mutates
overrides_ after finalisation while set_custom_assignment enforces immutability
via check_not_finalized(); call check_not_finalized() at the start of both
Context::set_override and Context::set_overrides to prevent post-finalisation
mutation and keep behavior consistent with set_custom_assignment (or, if
overrides are intentionally mutable, add explicit documentation to that effect
instead). Ensure you reference the check_not_finalized() guard and the
overrides_ mutations in these methods so tests expecting post-finalisation
mutation to be disallowed will pass.
| if (field.type == "number") { | ||
| try { | ||
| return std::stod(val); | ||
| } catch (...) { | ||
| return nullptr; | ||
| } |
There was a problem hiding this comment.
std::stod in custom_field_value has the same locale sensitivity issue as in evaluator.cpp.
For "number" type custom fields, std::stod is locale-dependent. This mirrors the issue flagged in evaluator.cpp. Consider using std::from_chars or a locale-independent parsing approach here as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.cpp` around lines 273 - 278, The code in custom_field_value uses
std::stod (in the block handling field.type == "number"), which is
locale-dependent; replace the std::stod call with a locale-independent parser
such as std::from_chars (include <charconv>), parse val into a double using
std::from_chars, check the returned error code (std::errc or
std::from_chars_result) and return nullptr on failure (same behavior as the
current catch path), and remove the try/catch; update the custom_field_value
function to handle the from_chars result and preserve the existing return
semantics.
| if (value.is_string()) { | ||
| const auto& str = value.get_ref<const std::string&>(); | ||
| if (str.empty()) { | ||
| return std::nullopt; | ||
| } | ||
| try { | ||
| std::size_t pos = 0; | ||
| double result = std::stod(str, &pos); | ||
| if (pos == str.size() && std::isfinite(result)) { | ||
| return result; | ||
| } | ||
| } catch (...) { | ||
| } | ||
| return std::nullopt; | ||
| } | ||
| return std::nullopt; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
std::from_chars double support compiler versions C++17 GCC Clang MSVC requirements
💡 Result:
std::from_chars for double (floating-point overloads) — practical support matrix (C++17)
| Toolchain | Standard library | Floating-point std::from_chars (incl. double) |
|---|---|---|
| GCC | libstdc++ | Implemented in libstdc++ trunk July 2020 (float/double/long double overloads were added then). (gcc.gnu.org) Later work switched float/double parsing to fast_float on IEEE binary32/64 targets. (gcc.gnu.org) |
| Clang | libc++ | libc++ reports: std::from_chars for float/double is available since libc++ v20; support is “complete except for long double”. (libcxx.llvm.org) |
| Clang | libstdc++ | Same as GCC’s libstdc++ version you’re linking against (so floating support depends on that libstdc++). (gcc.gnu.org) |
| MSVC | MSVC STL | Visual Studio 2017 15.8 implemented floating-point from_chars() in <charconv>. (devblogs.microsoft.com) |
Notes / requirements
- The floating overloads are the C++17 overloads in
<charconv>taking a floating type and an optionalstd::chars_format. (en.cppreference.com) - If you need a portable compile-time check, be careful: the usual feature-test macro situation has historically been imperfect in some libstdc++ configurations (headers/macros vs actual availability). (gcc.gnu.org)
Citations:
- 1: https://gcc.gnu.org/pipermail/libstdc%2B%2B/2020-July/050633.html?utm_source=openai
- 2: https://gcc.gnu.org/pipermail/gcc-patches/2022-January/588560.html?utm_source=openai
- 3: https://libcxx.llvm.org/Status/Cxx17.html?utm_source=openai
- 4: https://gcc.gnu.org/pipermail/libstdc%2B%2B/2020-July/050633.html?utm_source=openai
- 5: https://devblogs.microsoft.com/cppblog/stl-features-and-fixes-in-vs-2017-15-8/?utm_source=openai
- 6: https://en.cppreference.com/w/cpp/utility/from_chars.html?utm_source=openai
- 7: https://gcc.gnu.org/pipermail/gcc-patches/2022-March/591620.html?utm_source=openai
🌐 Web query:
std::stod locale sensitivity decimal separator C locale example
💡 Result:
std::stod is locale-sensitive because it is specified in terms of the C library conversion (strtod), which uses the C locale’s LC_NUMERIC category to decide what the decimal separator is. So, if your process locale uses a comma decimal point (e.g., many European locales), "1,5" may parse as 1.5 and "1.5" may stop at the . (or fail depending on input handling). (stackoverflow.com)
Minimal example (showing the effect)
`#include` <clocale>
`#include` <iostream>
`#include` <string>
int main() {
// Suppose your environment supports a comma-decimal locale like "de_DE.UTF-8".
std::setlocale(LC_NUMERIC, "de_DE.UTF-8");
std::cout << std::stod("1,5") << "\n"; // likely prints 1.5
// std::cout << std::stod("1.5") << "\n"; // may parse only "1" then stop at '.'
// Force "C" numeric rules: decimal point is '.'
std::setlocale(LC_NUMERIC, "C");
std::cout << std::stod("1.5") << "\n"; // prints 1.5
}LC_NUMERIC is the locale part that affects strtod/stod, and you can guarantee “C-locale” parsing by setting LC_NUMERIC (or LC_ALL) to "C". (open-std.org)
If you need locale-independent parsing (recommended)
- Prefer
std::from_charsfor floating-point (C++17/20; where implemented) since it’s not based on locales. - Or use a locale-fixed approach (e.g., a
std::istringstreamimbued withstd::locale::classic()), instead of changing the global C locale (which is process-global and problematic in multi-threaded code). (stackoverflow.com)
Citations:
- 1: https://stackoverflow.com/questions/12316972/how-to-tweak-stdstod-string-to-double-for-decimal-separator-and-number-of-di?utm_source=openai
- 2: https://www.open-std.org/jtc1/sc22/wg14/issues/c90/issue0024.html?utm_source=openai
- 3: https://stackoverflow.com/questions/12316972/how-to-tweak-stdstod-string-to-double-for-decimal-separator-and-number-of-di?utm_source=openai
Locale-sensitive string-to-number parsing may produce incorrect results.
std::stod respects the current C locale. If the application (or a third-party library) sets a locale that uses , as the decimal separator (e.g., de_DE), parsing "1.5" will yield 1.0 (stopping at the dot) and the pos == str.size() guard will return std::nullopt — silently breaking numeric string coercion. For an SDK that must behave deterministically, prefer a locale-independent approach.
🔧 Proposed fix using std::from_chars
std::from_chars (C++17, locale-independent by specification):
- try {
- std::size_t pos = 0;
- double result = std::stod(str, &pos);
- if (pos == str.size() && std::isfinite(result)) {
- return result;
- }
- } catch (...) {
- }
+ double result = 0.0;
+ auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
+ if (ec == std::errc{} && ptr == str.data() + str.size() && std::isfinite(result)) {
+ return result;
+ }Compiler support: GCC 11+ (libstdc++), Clang 16+ (libc++), MSVC 19.15+ (Visual Studio 2017 15.8+).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (value.is_string()) { | |
| const auto& str = value.get_ref<const std::string&>(); | |
| if (str.empty()) { | |
| return std::nullopt; | |
| } | |
| try { | |
| std::size_t pos = 0; | |
| double result = std::stod(str, &pos); | |
| if (pos == str.size() && std::isfinite(result)) { | |
| return result; | |
| } | |
| } catch (...) { | |
| } | |
| return std::nullopt; | |
| } | |
| return std::nullopt; | |
| } | |
| if (value.is_string()) { | |
| const auto& str = value.get_ref<const std::string&>(); | |
| if (str.empty()) { | |
| return std::nullopt; | |
| } | |
| double result = 0.0; | |
| auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result); | |
| if (ec == std::errc{} && ptr == str.data() + str.size() && std::isfinite(result)) { | |
| return result; | |
| } | |
| return std::nullopt; | |
| } | |
| return std::nullopt; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/json_expr/evaluator.cpp` around lines 84 - 100, The string-to-double
conversion uses locale-sensitive std::stod; replace it with locale-independent
std::from_chars (include <charconv>) to parse the std::string 'str' (or a
std::string_view) into a double named 'result', remove the try/catch and the pos
variable, then check that from_chars returned no error and consumed the entire
input (the end pointer equals str.data()+str.size()) and that
std::isfinite(result) before returning result; otherwise return std::nullopt —
update the conversion logic in the block that currently references value, str,
pos, result to use std::from_chars instead.
| if (value.is_number()) { | ||
| double d = value.get<double>(); | ||
| double int_part; | ||
| if (std::modf(d, &int_part) == 0.0 && std::abs(d) < 1e15) { | ||
| std::ostringstream oss; | ||
| oss << std::fixed << std::setprecision(0) << d; | ||
| return oss.str(); | ||
| } | ||
| std::ostringstream oss; | ||
| oss << std::setprecision(15) << d; | ||
| std::string result = oss.str(); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Locale-sensitive number-to-string formatting.
std::ostringstream uses the global locale by default. In locales with , as the decimal separator, to_string_value(1.5) would produce "1,5", breaking downstream string comparisons and audience matching. Imbue the stream with the classic "C" locale to ensure consistent output.
🔧 Proposed fix — imbue classic locale
if (value.is_number()) {
double d = value.get<double>();
double int_part;
if (std::modf(d, &int_part) == 0.0 && std::abs(d) < 1e15) {
std::ostringstream oss;
+ oss.imbue(std::locale::classic());
oss << std::fixed << std::setprecision(0) << d;
return oss.str();
}
std::ostringstream oss;
+ oss.imbue(std::locale::classic());
oss << std::setprecision(15) << d;
std::string result = oss.str();
return result;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/json_expr/evaluator.cpp` around lines 112 - 124, The number-to-string
path in src/json_expr/evaluator.cpp uses std::ostringstream (the two local
variables named oss) which pick up the global locale; imbue each ostringstream
with the C locale (call oss.imbue(std::locale::classic()) immediately after
construction) before formatting so decimal separators are always '.'; also
ensure <locale> is included if not already.
| for (; w < 16; ++w) { | ||
| block[w] = 0; | ||
| } | ||
|
|
||
| block[14] = static_cast<uint32_t>(len << 3); | ||
| md5cycle(state, block); |
There was a problem hiding this comment.
Encode the MD5 bit‑length as 64‑bit to avoid truncation.
On Line 173 the length is written as a 32‑bit value, so inputs ≥ 512 MB will hash incorrectly. MD5 appends a 64‑bit bit‑length; write both low and high words.
🔧 Suggested update
- block[14] = static_cast<uint32_t>(len << 3);
- md5cycle(state, block);
+ const uint64_t bit_len = static_cast<uint64_t>(len) << 3;
+ block[14] = static_cast<uint32_t>(bit_len & 0xFFFFFFFFu);
+ block[15] = static_cast<uint32_t>(bit_len >> 32);
+ md5cycle(state, block);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (; w < 16; ++w) { | |
| block[w] = 0; | |
| } | |
| block[14] = static_cast<uint32_t>(len << 3); | |
| md5cycle(state, block); | |
| for (; w < 16; ++w) { | |
| block[w] = 0; | |
| } | |
| const uint64_t bit_len = static_cast<uint64_t>(len) << 3; | |
| block[14] = static_cast<uint32_t>(bit_len & 0xFFFFFFFFu); | |
| block[15] = static_cast<uint32_t>(bit_len >> 32); | |
| md5cycle(state, block); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/md5.cpp` around lines 169 - 174, The code currently writes only a 32-bit
length into block[14], truncating inputs ≥512MB; compute the full 64-bit
bit-length (e.g. uint64_t bitlen = static_cast<uint64_t>(len) << 3), then store
the low 32 bits into block[14] and the high 32 bits into block[15] before
calling md5cycle(state, block) so the MD5 length is encoded as a 64-bit value.
Add SDK::create() factory and sdk->create_context() / create_context_with() convenience methods for simpler initialization.
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
README.md (2)
11-37:⚠️ Potential issue | 🟡 MinorFix heading level jumps in the Installation section.
Lines 11, 28, and 37 jump from H2 to H4, which violates markdownlint's heading increment rule. These subsection headings should be H3.
📝 Proposed change
-#### CMake (FetchContent) +### CMake (FetchContent) Add the following to your `CMakeLists.txt` to include the SDK directly from the repository: @@ -25,7 +25,7 @@ target_link_libraries(your_target PRIVATE absmartly-sdk)-#### CMake (Local)
+### CMake (Local)If you have cloned the repository locally, you can add it as a subdirectory:
@@ -34,7 +34,7 @@
target_link_libraries(your_target PRIVATE absmartly-sdk)-#### Building from Source +### Building from Source🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 11 - 37, Change the three subsection headings currently using H4 (the lines titled "CMake (FetchContent)", "CMake (Local)", and "Building from Source") to H3 so they consistently increment from the H2 "Installation" heading; update the heading markers from #### to ### for the symbols "CMake (FetchContent)", "CMake (Local)", and "Building from Source" and verify the surrounding blocks keep the same content and spacing.
238-238:⚠️ Potential issue | 🟡 MinorAdd comma for improved readability in compound sentence.
The sentence connects two independent clauses and would read more smoothly with a comma before "and".
📝 Proposed change
-You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context. +You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 238, Insert a comma before the conjunction "and" in the sentence "This is useful when a user logs in and you want to associate the new identity with the context." so it reads "This is useful when a user logs in, and you want to associate the new identity with the context." to improve readability; update the README.md sentence accordingly.
🧹 Nitpick comments (7)
README.md (1)
21-21: Consider recommending a pinned version instead of "main".Using
GIT_TAG mainin the FetchContent example will fetch the latest commit from the main branch, which could introduce breaking changes unexpectedly and makes builds non-reproducible. For production use, it's better to recommend a specific tag or commit SHA.📌 Suggested alternative
FetchContent_Declare( absmartly-sdk GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git - GIT_TAG main + GIT_TAG v1.0.0 # or a specific commit SHA )Alternatively, add a note in the documentation explaining that users should replace
mainwith a specific version tag for production use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 21, The README uses an unstable branch reference "GIT_TAG main" which risks non-reproducible builds; update the documentation to recommend pinning to a specific tag or commit (e.g., replace "GIT_TAG main" with a version tag or commit SHA) and add a brief note advising users to use a fixed tag/sha for production to ensure reproducible builds.CMakeLists.txt (2)
4-5: Consider setting C++ standard per-target instead of globally.Setting
CMAKE_CXX_STANDARDglobally affects all targets in the project. For better encapsulation, consider usingtarget_compile_features(absmartly-sdk PUBLIC cxx_std_17)instead, which also ensures consumers use C++17.♻️ Proposed refactor
-set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +# C++ standard set per-target belowThen after
add_library:target_link_libraries(absmartly-sdk PUBLIC nlohmann_json::nlohmann_json) +target_compile_features(absmartly-sdk PUBLIC cxx_std_17)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CMakeLists.txt` around lines 4 - 5, The CMake file currently sets C++17 globally via CMAKE_CXX_STANDARD; instead, remove or stop relying on the global setting and set the standard per-target by adding target_compile_features(absmartly-sdk PUBLIC cxx_std_17) immediately after the add_library(...) that defines the absmartly-sdk target, so the absmartly-sdk target and its consumers are required to use C++17 without impacting other targets.
47-47: Consider defaultingABSMARTLY_BUILD_TESTSto OFF.Having tests enabled by default means consumers who use
add_subdirectoryor FetchContent will build tests unnecessarily. A common convention is to default tests to OFF for library projects, or enable them only when built as the top-level project.♻️ Proposed refactor
-option(ABSMARTLY_BUILD_TESTS "Build tests" ON) +option(ABSMARTLY_BUILD_TESTS "Build tests" OFF) + +# Or conditionally enable when this is the main project: +# if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) +# set(ABSMARTLY_BUILD_TESTS_DEFAULT ON) +# else() +# set(ABSMARTLY_BUILD_TESTS_DEFAULT OFF) +# endif() +# option(ABSMARTLY_BUILD_TESTS "Build tests" ${ABSMARTLY_BUILD_TESTS_DEFAULT})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CMakeLists.txt` at line 47, Change the option ABSMARTLY_BUILD_TESTS to default OFF instead of ON; update the CMakeLists to declare option(ABSMARTLY_BUILD_TESTS "Build tests" OFF) and, if you want to keep the old top-level behavior, wrap a top-level check such as comparing CMAKE_SOURCE_DIR and CMAKE_CURRENT_SOURCE_DIR to set it to ON only when the project is the top-level build (or conditionally add tests only when that variable is true). Ensure references to ABSMARTLY_BUILD_TESTS in add_subdirectory/test-target logic remain unchanged.include/absmartly/context.h (2)
41-44: Document ownership semantics for raw pointer inExperimentIndex.
ExperimentIndex::datais a raw pointer toExperimentData. The ownership relationship should be documented—presumably this points intodata_.experimentsand is only valid whilst the parentContextis alive. Consider adding a comment to clarify this invariant.📝 Suggested documentation
struct ExperimentIndex { - const ExperimentData* data = nullptr; + const ExperimentData* data = nullptr; // Non-owning; points into Context::data_.experiments std::vector<std::map<std::string, nlohmann::json>> variables; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@include/absmartly/context.h` around lines 41 - 44, Document that ExperimentIndex::data is a non-owning pointer: add a concise comment on the ExperimentIndex struct stating that data is a raw, non-owning pointer to an ExperimentData that lives inside the parent Context (e.g., Context::data_.experiments) and is only valid while that Context remains alive; mention the invariant that ExperimentIndex instances must not outlive their owning Context and that no deletion is performed through this pointer.
12-12:<set>header appears unused in this header file.The
<set>include is not used in any declarations here. If it is only needed in the implementation (src/context.cpp), move it there to reduce header dependencies and compilation times.♻️ Proposed fix
`#include` <map> -#include <set> `#include` <vector>Then add
#include <set>insrc/context.cppif needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@include/absmartly/context.h` at line 12, Remove the unused `#include` <set> from the context.h header and, if any std::set usage exists in the implementation of the context declarations, add `#include` <set> to the corresponding source file that implements those functions (the context.cpp implementation). This reduces header dependencies and keeps the header minimal.src/context.cpp (2)
14-20: Floating-point exact equality comparison may be fragile.
array_equals_shallowuses==to comparedoublevalues. While this may be intentional for comparing stored traffic splits that originated from the same source, floating-point exact equality can be problematic if values are ever computed differently. Consider whether an epsilon-based comparison is warranted, or add a comment explaining why exact equality is safe here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 14 - 20, The function array_equals_shallow uses exact equality on doubles which is brittle; update array_equals_shallow to perform comparisons with a small relative/absolute epsilon (or use std::fabs(a[i]-b[i]) <= epsilon) to tolerate floating-point rounding, and choose/define a sensible EPSILON constant nearby (or accept an optional tolerance parameter); alternatively, if exact bitwise equality is intended, add a clear comment in array_equals_shallow explaining why exact equality is safe and under what guarantees (e.g., values always come from the same source) so future maintainers won’t change it.
56-59: Redundant assignment ofdata_ininit().
become_ready()already movesdataintodata_at line 57, then callsinit(data_)which assignsdata_again at line 618. The second assignment is redundant.♻️ Proposed fix
void Context::init(const ContextData& data) { - data_ = data; build_index(); }Or alternatively, if
initis intended to be callable independently:void Context::become_ready(ContextData data) { - data_ = std::move(data); - init(data_); + init(std::move(data)); ready_ = true; // ... } -void Context::init(const ContextData& data) { - data_ = data; +void Context::init(ContextData data) { + data_ = std::move(data); build_index(); }Also applies to: 617-620
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context.cpp` around lines 56 - 59, Context::become_ready currently moves the incoming ContextData into data_ then calls init(data_), but init also reassigns data_ (redundant); remove the duplicate assignment inside Context::init (the data_ = std::move(data) or similar) and have init operate on the already-stored data_ (or change init to take no parameter), leaving Context::become_ready as data_ = std::move(data); init(); ready_ = true; so the double move/assignment is eliminated; update references to the parameter inside Context::init accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@include/absmartly/sdk.h`:
- Around line 1-7: Add the missing <future> include so std::future is available:
update the header (include/absmartly/sdk.h) to `#include` <future> near the other
includes, ensuring the declaration std::future<ContextData> get_context_data()
compiles; keep the include order consistent with existing standard headers and
avoid adding other code changes.
- Line 3: The SDK references an undefined type SDKConfig and includes a missing
header; create a new header that defines the SDKConfig type (e.g., a simple
struct with the configuration fields used by class SDK and any defaults),
protect it with include guards or `#pragma` once, and ensure it declares anything
the SDK methods (constructor/factory methods) expect; then keep the existing
`#include` <absmartly/sdk_config.h> in the SDK header so SDK can compile. If you
prefer not to add a header, remove the `#include` and replace SDKConfig usages
inside class SDK with a suitable existing type or a forward declaration and
update the SDK methods (e.g., SDK::create, SDK constructor) accordingly.
In `@README.md`:
- Around line 455-469: The README links for each SDK (e.g., link items like
"[Java SDK]", "[JavaScript SDK]", "[C++ SDK]" etc.) use non-canonical URLs with
the "www" subdomain (https://www.github.com/...). Update each SDK link target to
use the canonical GitHub domain by removing "www" so they read
https://github.com/absmartly/<repo>, ensuring every listed link (Java,
JavaScript, PHP, Swift, Vue2, Vue3, React, Angular, Python3, Go, Ruby, .NET,
C++, Dart, Flutter) is changed accordingly.
In `@src/context.cpp`:
- Around line 70-88: The wait_until_ready() implementation calls
data_future_.get() which is single-use; change data_future_ from std::future<T>
to std::shared_future<T> (update its declaration and wherever it is
assigned/constructed) so multiple calls to Context::wait_until_ready() are safe,
then replace .get() usage in wait_until_ready() with the shared_future's .get();
ensure constructors or factory functions that produce the future convert or call
.share() to initialize data_future_, and keep existing logic around ready_,
failed_, become_ready, and emit_event unchanged.
- Around line 644-645: The silent catch(...) in src/context.cpp that wraps
parsing of variant.config should be replaced with a typed catch that logs the
error and the offending config so parse failures are discoverable: catch
std::exception (e.g., catch (const std::exception& e)) and call your existing
logging/emitter (e.g., processLogger.warn or emitEvent) including e.what() and
the raw variant.config (and variant identifier if available); if non-std
exceptions must be handled too, add a final catch(...) that logs a generic parse
failure. Ensure the log message clearly names the operation (parsing
variant.config) and includes the value and error details.
- Around line 382-418: The Context class (methods like publish(), and members
pending_, exposures_, goals_, attrs_, units_) is not thread-safe; either add
synchronization or document single-threaded usage: to fix, add a std::mutex
member to Context (e.g., mutex_), protect all accesses and mutations of
pending_, exposures_, goals_, attrs_, and units_ by locking (use
std::lock_guard<std::mutex> or std::scoped_lock) in publish() and any other
methods that read/write these members, or alternatively add a clear comment in
the class header and README stating Context must only be used from a single
thread and ensure all call sites are single-threaded.
---
Duplicate comments:
In `@README.md`:
- Around line 11-37: Change the three subsection headings currently using H4
(the lines titled "CMake (FetchContent)", "CMake (Local)", and "Building from
Source") to H3 so they consistently increment from the H2 "Installation"
heading; update the heading markers from #### to ### for the symbols "CMake
(FetchContent)", "CMake (Local)", and "Building from Source" and verify the
surrounding blocks keep the same content and spacing.
- Line 238: Insert a comma before the conjunction "and" in the sentence "This is
useful when a user logs in and you want to associate the new identity with the
context." so it reads "This is useful when a user logs in, and you want to
associate the new identity with the context." to improve readability; update the
README.md sentence accordingly.
---
Nitpick comments:
In `@CMakeLists.txt`:
- Around line 4-5: The CMake file currently sets C++17 globally via
CMAKE_CXX_STANDARD; instead, remove or stop relying on the global setting and
set the standard per-target by adding target_compile_features(absmartly-sdk
PUBLIC cxx_std_17) immediately after the add_library(...) that defines the
absmartly-sdk target, so the absmartly-sdk target and its consumers are required
to use C++17 without impacting other targets.
- Line 47: Change the option ABSMARTLY_BUILD_TESTS to default OFF instead of ON;
update the CMakeLists to declare option(ABSMARTLY_BUILD_TESTS "Build tests" OFF)
and, if you want to keep the old top-level behavior, wrap a top-level check such
as comparing CMAKE_SOURCE_DIR and CMAKE_CURRENT_SOURCE_DIR to set it to ON only
when the project is the top-level build (or conditionally add tests only when
that variable is true). Ensure references to ABSMARTLY_BUILD_TESTS in
add_subdirectory/test-target logic remain unchanged.
In `@include/absmartly/context.h`:
- Around line 41-44: Document that ExperimentIndex::data is a non-owning
pointer: add a concise comment on the ExperimentIndex struct stating that data
is a raw, non-owning pointer to an ExperimentData that lives inside the parent
Context (e.g., Context::data_.experiments) and is only valid while that Context
remains alive; mention the invariant that ExperimentIndex instances must not
outlive their owning Context and that no deletion is performed through this
pointer.
- Line 12: Remove the unused `#include` <set> from the context.h header and, if
any std::set usage exists in the implementation of the context declarations, add
`#include` <set> to the corresponding source file that implements those functions
(the context.cpp implementation). This reduces header dependencies and keeps the
header minimal.
In `@README.md`:
- Line 21: The README uses an unstable branch reference "GIT_TAG main" which
risks non-reproducible builds; update the documentation to recommend pinning to
a specific tag or commit (e.g., replace "GIT_TAG main" with a version tag or
commit SHA) and add a brief note advising users to use a fixed tag/sha for
production to ensure reproducible builds.
In `@src/context.cpp`:
- Around line 14-20: The function array_equals_shallow uses exact equality on
doubles which is brittle; update array_equals_shallow to perform comparisons
with a small relative/absolute epsilon (or use std::fabs(a[i]-b[i]) <= epsilon)
to tolerate floating-point rounding, and choose/define a sensible EPSILON
constant nearby (or accept an optional tolerance parameter); alternatively, if
exact bitwise equality is intended, add a clear comment in array_equals_shallow
explaining why exact equality is safe and under what guarantees (e.g., values
always come from the same source) so future maintainers won’t change it.
- Around line 56-59: Context::become_ready currently moves the incoming
ContextData into data_ then calls init(data_), but init also reassigns data_
(redundant); remove the duplicate assignment inside Context::init (the data_ =
std::move(data) or similar) and have init operate on the already-stored data_
(or change init to take no parameter), leaving Context::become_ready as data_ =
std::move(data); init(); ready_ = true; so the double move/assignment is
eliminated; update references to the parameter inside Context::init accordingly.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
CMakeLists.txtREADME.mdinclude/absmartly/context.hinclude/absmartly/sdk.hsrc/context.cpp
| #pragma once | ||
|
|
||
| #include <absmartly/sdk_config.h> | ||
| #include <absmartly/context.h> | ||
| #include <absmartly/context_config.h> | ||
| #include <memory> | ||
|
|
There was a problem hiding this comment.
Missing <future> header for std::future usage.
Line 17 declares std::future<ContextData> get_context_data() but <future> is not included. This will cause a compilation error.
🐛 Proposed fix
`#include` <absmartly/sdk_config.h>
`#include` <absmartly/context.h>
`#include` <absmartly/context_config.h>
`#include` <memory>
+#include <future>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #pragma once | |
| #include <absmartly/sdk_config.h> | |
| #include <absmartly/context.h> | |
| #include <absmartly/context_config.h> | |
| #include <memory> | |
| `#pragma` once | |
| `#include` <absmartly/sdk_config.h> | |
| `#include` <absmartly/context.h> | |
| `#include` <absmartly/context_config.h> | |
| `#include` <memory> | |
| `#include` <future> | |
🧰 Tools
🪛 Clang (14.0.6)
[error] 3-3: 'absmartly/sdk_config.h' file not found
(clang-diagnostic-error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@include/absmartly/sdk.h` around lines 1 - 7, Add the missing <future> include
so std::future is available: update the header (include/absmartly/sdk.h) to
`#include` <future> near the other includes, ensuring the declaration
std::future<ContextData> get_context_data() compiles; keep the include order
consistent with existing standard headers and avoid adding other code changes.
| @@ -0,0 +1,28 @@ | |||
| #pragma once | |||
|
|
|||
| #include <absmartly/sdk_config.h> | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for sdk_config.h anywhere in the repository
echo "=== Searching for sdk_config.h in the entire repository ==="
fd -t f "sdk_config.h" .
echo ""
echo "=== Checking include/absmartly/ directory structure ==="
fd -t f . include/absmartly/ | head -20Repository: absmartly/cpp-sdk
Length of output: 542
🏁 Script executed:
#!/bin/bash
# Check the content of sdk.h to see the context around the include
cat -n include/absmartly/sdk.h | head -30Repository: absmartly/cpp-sdk
Length of output: 1027
🏁 Script executed:
#!/bin/bash
# Search for SDKConfig definition in the codebase
echo "=== Searching for SDKConfig definition ==="
rg -n "class SDKConfig|struct SDKConfig" include/
echo ""
echo "=== Searching for SDKConfig usage ==="
rg -n "SDKConfig" include/ | head -20Repository: absmartly/cpp-sdk
Length of output: 290
🏁 Script executed:
#!/bin/bash
# Search for SDKConfig definition in the entire repository, including src/
echo "=== Searching for SDKConfig definition in entire repository ==="
rg -n "class SDKConfig|struct SDKConfig" .
echo ""
echo "=== Searching for sdk_config files (any variant) ==="
fd -t f "*sdk_config*" .Repository: absmartly/cpp-sdk
Length of output: 487
🏁 Script executed:
#!/bin/bash
# Search for SDKConfig definition in entire repository with correct syntax
echo "=== Searching for SDKConfig definition in entire repository ==="
rg -n "class SDKConfig|struct SDKConfig" . --type cpp --type h
echo ""
echo "=== Searching for sdk_config files ==="
fd -t f --fixed-strings "sdk_config" .
echo ""
echo "=== Listing all header files in include/absmartly/ ==="
ls -la include/absmartly/Repository: absmartly/cpp-sdk
Length of output: 955
🏁 Script executed:
#!/bin/bash
# Check context_config.h to see if there's a clue about SDKConfig
echo "=== Content of context_config.h ==="
cat -n include/absmartly/context_config.h
echo ""
echo "=== Searching for SDKConfig in source files ==="
fd -t f . src/ 2>/dev/null | head -20
echo ""
echo "=== Check if src directory exists ==="
ls -la src/ 2>/dev/null || echo "src/ directory not found"Repository: absmartly/cpp-sdk
Length of output: 1328
🏁 Script executed:
#!/bin/bash
# Search for SDKConfig definition in source files
echo "=== Searching for SDKConfig in all files ==="
rg -n "SDKConfig" . --type cpp --type h -A 3 -B 1
echo ""
echo "=== Check for any .hpp files ==="
fd -t f --fixed-strings ".hpp" include/Repository: absmartly/cpp-sdk
Length of output: 818
Create sdk_config.h or remove the missing include.
The header file include/absmartly/sdk_config.h does not exist in the repository. The SDK class declares methods using the undefined SDKConfig type (lines 12, 20), which will cause compilation failure. Either the header file must be created with the SDKConfig struct definition, or the include and type references must be removed or corrected.
🧰 Tools
🪛 Clang (14.0.6)
[error] 3-3: 'absmartly/sdk_config.h' file not found
(clang-diagnostic-error)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@include/absmartly/sdk.h` at line 3, The SDK references an undefined type
SDKConfig and includes a missing header; create a new header that defines the
SDKConfig type (e.g., a simple struct with the configuration fields used by
class SDK and any defaults), protect it with include guards or `#pragma` once, and
ensure it declares anything the SDK methods (constructor/factory methods)
expect; then keep the existing `#include` <absmartly/sdk_config.h> in the SDK
header so SDK can compile. If you prefer not to add a header, remove the
`#include` and replace SDKConfig usages inside class SDK with a suitable existing
type or a forward declaration and update the SDK methods (e.g., SDK::create, SDK
constructor) accordingly.
| - [Java SDK](https://www.github.com/absmartly/java-sdk) | ||
| - [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk) | ||
| - [PHP SDK](https://www.github.com/absmartly/php-sdk) | ||
| - [Swift SDK](https://www.github.com/absmartly/swift-sdk) | ||
| - [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk) | ||
| - [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk) | ||
| - [React SDK](https://www.github.com/absmartly/react-sdk) | ||
| - [Angular SDK](https://www.github.com/absmartly/angular-sdk) | ||
| - [Python3 SDK](https://www.github.com/absmartly/python3-sdk) | ||
| - [Go SDK](https://www.github.com/absmartly/go-sdk) | ||
| - [Ruby SDK](https://www.github.com/absmartly/ruby-sdk) | ||
| - [.NET SDK](https://www.github.com/absmartly/dotnet-sdk) | ||
| - [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package) | ||
| - [Dart SDK](https://www.github.com/absmartly/dart-sdk) | ||
| - [Flutter SDK](https://www.github.com/absmartly/flutter-sdk) |
There was a problem hiding this comment.
Use canonical GitHub URLs without "www" subdomain.
All SDK links include the "www" subdomain (https://www.github.com/...), but GitHub's canonical domain is github.com without "www". Whilst the links work due to redirects, it's better to use the canonical form.
🔗 Proposed change
-### Have a look at our growing list of clients and SDKs:
-- [Java SDK](https://www.github.com/absmartly/java-sdk)
-- [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk)
-- [PHP SDK](https://www.github.com/absmartly/php-sdk)
-- [Swift SDK](https://www.github.com/absmartly/swift-sdk)
-- [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk)
-- [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk)
-- [React SDK](https://www.github.com/absmartly/react-sdk)
-- [Angular SDK](https://www.github.com/absmartly/angular-sdk)
-- [Python3 SDK](https://www.github.com/absmartly/python3-sdk)
-- [Go SDK](https://www.github.com/absmartly/go-sdk)
-- [Ruby SDK](https://www.github.com/absmartly/ruby-sdk)
-- [.NET SDK](https://www.github.com/absmartly/dotnet-sdk)
-- [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package)
-- [Dart SDK](https://www.github.com/absmartly/dart-sdk)
-- [Flutter SDK](https://www.github.com/absmartly/flutter-sdk)
+### Have a look at our growing list of clients and SDKs:
+- [Java SDK](https://github.com/absmartly/java-sdk)
+- [JavaScript SDK](https://github.com/absmartly/javascript-sdk)
+- [PHP SDK](https://github.com/absmartly/php-sdk)
+- [Swift SDK](https://github.com/absmartly/swift-sdk)
+- [Vue2 SDK](https://github.com/absmartly/vue2-sdk)
+- [Vue3 SDK](https://github.com/absmartly/vue3-sdk)
+- [React SDK](https://github.com/absmartly/react-sdk)
+- [Angular SDK](https://github.com/absmartly/angular-sdk)
+- [Python3 SDK](https://github.com/absmartly/python3-sdk)
+- [Go SDK](https://github.com/absmartly/go-sdk)
+- [Ruby SDK](https://github.com/absmartly/ruby-sdk)
+- [.NET SDK](https://github.com/absmartly/dotnet-sdk)
+- [C++ SDK](https://github.com/absmartly/cpp-sdk) (this package)
+- [Dart SDK](https://github.com/absmartly/dart-sdk)
+- [Flutter SDK](https://github.com/absmartly/flutter-sdk)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - [Java SDK](https://www.github.com/absmartly/java-sdk) | |
| - [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk) | |
| - [PHP SDK](https://www.github.com/absmartly/php-sdk) | |
| - [Swift SDK](https://www.github.com/absmartly/swift-sdk) | |
| - [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk) | |
| - [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk) | |
| - [React SDK](https://www.github.com/absmartly/react-sdk) | |
| - [Angular SDK](https://www.github.com/absmartly/angular-sdk) | |
| - [Python3 SDK](https://www.github.com/absmartly/python3-sdk) | |
| - [Go SDK](https://www.github.com/absmartly/go-sdk) | |
| - [Ruby SDK](https://www.github.com/absmartly/ruby-sdk) | |
| - [.NET SDK](https://www.github.com/absmartly/dotnet-sdk) | |
| - [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package) | |
| - [Dart SDK](https://www.github.com/absmartly/dart-sdk) | |
| - [Flutter SDK](https://www.github.com/absmartly/flutter-sdk) | |
| ### Have a look at our growing list of clients and SDKs: | |
| - [Java SDK](https://github.com/absmartly/java-sdk) | |
| - [JavaScript SDK](https://github.com/absmartly/javascript-sdk) | |
| - [PHP SDK](https://github.com/absmartly/php-sdk) | |
| - [Swift SDK](https://github.com/absmartly/swift-sdk) | |
| - [Vue2 SDK](https://github.com/absmartly/vue2-sdk) | |
| - [Vue3 SDK](https://github.com/absmartly/vue3-sdk) | |
| - [React SDK](https://github.com/absmartly/react-sdk) | |
| - [Angular SDK](https://github.com/absmartly/angular-sdk) | |
| - [Python3 SDK](https://github.com/absmartly/python3-sdk) | |
| - [Go SDK](https://github.com/absmartly/go-sdk) | |
| - [Ruby SDK](https://github.com/absmartly/ruby-sdk) | |
| - [.NET SDK](https://github.com/absmartly/dotnet-sdk) | |
| - [C++ SDK](https://github.com/absmartly/cpp-sdk) (this package) | |
| - [Dart SDK](https://github.com/absmartly/dart-sdk) | |
| - [Flutter SDK](https://github.com/absmartly/flutter-sdk) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 455 - 469, The README links for each SDK (e.g., link
items like "[Java SDK]", "[JavaScript SDK]", "[C++ SDK]" etc.) use non-canonical
URLs with the "www" subdomain (https://www.github.com/...). Update each SDK link
target to use the canonical GitHub domain by removing "www" so they read
https://github.com/absmartly/<repo>, ensuring every listed link (Java,
JavaScript, PHP, Swift, Vue2, Vue3, React, Angular, Python3, Go, Ruby, .NET,
C++, Dart, Flutter) is changed accordingly.
| void Context::wait_until_ready() { | ||
| if (ready_ || failed_) { | ||
| return; | ||
| } | ||
|
|
||
| if (!data_future_.valid()) { | ||
| failed_ = true; | ||
| emit_event("error", {{"message", "No data future available"}}); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| auto data = data_future_.get(); | ||
| become_ready(std::move(data)); | ||
| } catch (const std::exception& e) { | ||
| failed_ = true; | ||
| emit_event("error", {{"message", e.what()}}); | ||
| } | ||
| } |
There was a problem hiding this comment.
wait_until_ready() can only be called once due to std::future::get() semantics.
std::future::get() can only be called once; subsequent calls result in undefined behaviour or an exception (depending on implementation). If wait_until_ready() is called twice before the future resolves, the second call will fail. Consider using std::shared_future if multiple waits are expected, or document that this method must only be called once.
♻️ Alternative using shared_future
In header:
- std::future<ContextData> data_future_;
+ std::shared_future<ContextData> data_future_;In constructor:
- , data_future_(std::move(data_future)) {
+ , data_future_(data_future.share()) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.cpp` around lines 70 - 88, The wait_until_ready() implementation
calls data_future_.get() which is single-use; change data_future_ from
std::future<T> to std::shared_future<T> (update its declaration and wherever it
is assigned/constructed) so multiple calls to Context::wait_until_ready() are
safe, then replace .get() usage in wait_until_ready() with the shared_future's
.get(); ensure constructors or factory functions that produce the future convert
or call .share() to initialize data_future_, and keep existing logic around
ready_, failed_, become_ready, and emit_event unchanged.
| PublishEvent Context::publish() { | ||
| if (pending_ == 0) { | ||
| return {}; | ||
| } | ||
|
|
||
| PublishEvent event; | ||
| event.hashed = true; | ||
| event.publishedAt = now_millis(); | ||
|
|
||
| for (const auto& [type, uid] : units_) { | ||
| Unit u; | ||
| u.type = type; | ||
| u.uid = unit_hash(type); | ||
| event.units.push_back(std::move(u)); | ||
| } | ||
|
|
||
| if (!exposures_.empty()) { | ||
| event.exposures = std::move(exposures_); | ||
| exposures_.clear(); | ||
| } | ||
|
|
||
| if (!goals_.empty()) { | ||
| event.goals = std::move(goals_); | ||
| goals_.clear(); | ||
| } | ||
|
|
||
| if (!attrs_.empty()) { | ||
| event.attributes = attrs_; | ||
| } | ||
|
|
||
| pending_ = 0; | ||
|
|
||
| nlohmann::json pub_data = event; | ||
| emit_event("publish", pub_data); | ||
|
|
||
| return event; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the Context class definition
fd -t f -e h context.h | head -5Repository: absmartly/cpp-sdk
Length of output: 88
🏁 Script executed:
# Search for thread-related documentation and synchronisation keywords
rg -i "thread|mutex|lock|atomic" include/absmartly/ --type cpp --type hRepository: absmartly/cpp-sdk
Length of output: 43
🏁 Script executed:
# Examine the full context.h file to understand the class structure
cat include/absmartly/context.hRepository: absmartly/cpp-sdk
Length of output: 5254
🏁 Script executed:
# Look at the context.cpp file around the publish() method and check for any synchronisation
sed -n '1,50p' src/context.cppRepository: absmartly/cpp-sdk
Length of output: 1650
🏁 Script executed:
# Check if there's a README or documentation about thread-safety
find . -maxdepth 2 -name "README*" -o -name "*.md" | head -10Repository: absmartly/cpp-sdk
Length of output: 72
🏁 Script executed:
# Read the README to check for thread-safety documentation
cat README.mdRepository: absmartly/cpp-sdk
Length of output: 16471
🏁 Script executed:
# Search more broadly in the entire src/context.cpp file for synchronisation
rg "mutex|lock|atomic|thread" src/context.cppRepository: absmartly/cpp-sdk
Length of output: 109
🏁 Script executed:
# Check how Context is constructed and used in tests/examples
fd -t f -e cpp -e h test examples | head -20Repository: absmartly/cpp-sdk
Length of output: 156
🏁 Script executed:
# Search for any documentation comments in context.cpp about thread-safety
rg -B5 -A5 "publish\(" src/context.cpp | head -40Repository: absmartly/cpp-sdk
Length of output: 406
🏁 Script executed:
# Check if there are any member variables that suggest thread-safety (like mutex)
rg "std::mutex|std::lock|std::atomic" include/absmartly/Repository: absmartly/cpp-sdk
Length of output: 43
publish() (and the entire Context class) is not thread-safe.
publish() and other methods read and modify shared state (pending_, exposures_, goals_, attrs_, units_) without synchronisation. The class contains no synchronisation primitives (no std::mutex, std::atomic, or locks). Data races will occur if Context is accessed concurrently from multiple threads. Either:
- Add thread-safe synchronisation if multi-threaded use is intended, or
- Document prominently that
Contextis not thread-safe and must be used from a single thread only.
The README provides no guidance on this constraint, and the usage examples suggest single-threaded usage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.cpp` around lines 382 - 418, The Context class (methods like
publish(), and members pending_, exposures_, goals_, attrs_, units_) is not
thread-safe; either add synchronization or document single-threaded usage: to
fix, add a std::mutex member to Context (e.g., mutex_), protect all accesses and
mutations of pending_, exposures_, goals_, attrs_, and units_ by locking (use
std::lock_guard<std::mutex> or std::scoped_lock) in publish() and any other
methods that read/write these members, or alternatively add a clear comment in
the class header and README stating Context must only be used from a single
thread and ensure all call sites are single-threaded.
| } catch (...) { | ||
| } |
There was a problem hiding this comment.
Silent catch block hides JSON parse errors.
If variant.config contains invalid JSON, the error is silently ignored. Consider logging a warning or emitting an event so configuration issues are discoverable during debugging.
🛠️ Suggested improvement
try {
auto config_json = nlohmann::json::parse(config_str);
if (config_json.is_object()) {
for (auto it = config_json.begin(); it != config_json.end(); ++it) {
parsed[it.key()] = it.value();
}
}
- } catch (...) {
+ } catch (const std::exception& e) {
+ // Optionally log or emit an event about malformed config
+ // e.g., emit_event("warning", {{"message", "Failed to parse variant config"}, {"error", e.what()}});
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/context.cpp` around lines 644 - 645, The silent catch(...) in
src/context.cpp that wraps parsing of variant.config should be replaced with a
typed catch that logs the error and the offending config so parse failures are
discoverable: catch std::exception (e.g., catch (const std::exception& e)) and
call your existing logging/emitter (e.g., processLogger.warn or emitEvent)
including e.what() and the raw variant.config (and variant identifier if
available); if non-std exceptions must be handled too, add a final catch(...)
that logs a generic parse failure. Ensure the log message clearly names the
operation (parsing variant.config) and includes the value and error details.
Summary
Test plan
Summary by CodeRabbit
New Features
Documentation
Tests