Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convenience methods to serialize attributes to JSON string and other cleanup #10

Merged
merged 4 commits into from
Mar 22, 2024

Conversation

aarsilv
Copy link
Collaborator

@aarsilv aarsilv commented Mar 22, 2024

Eppo Internal:
๐ŸŽŸ๏ธ Ticket 1: FF-1648 - Add more isXXX checks for EppoValue (and ideally a JSON serialize)
๐ŸŽŸ๏ธ Ticket 2: FF-1764 - Log "Bandit Key" in the bandit logger instead of "Variation"

For a user implementing our loggers, they will most likely need to serialize attributes to a JSON string when storing subject attributes for the assignment and subject and action numeric and categorical attributes for the bandit assignment.

This PR adds two new static utility functions to EppoAttributes:

  • serializeAttributesToJSONString()
  • serializeNonNullAttributesToJSONString()
    There is also a convenience instance method added to EppoAttributes:
  • serializeAttributesToJSONString()

For better logger clarity, we've also renamed variation to banditKey.

Example usage by the Bandit Proxy:

private static EppoClient initClient() {
    String apiKey = dotenv.get("EPPO_SDK_KEY");
    EppoClientConfig config = EppoClientConfig.builder()
      .apiKey(apiKey)
      .assignmentLogger(assignmentLogData -> {
        String sql = "INSERT INTO bandit_test_assignments " +
          "(timestamp, flag_name, experiment_name, variation_value, subject_key, subject_attributes) " +
          "SELECT ?, ?, ?, ?, ?, parse_json(?)";

        try (PreparedStatement statement = snowflakeConnection.prepareStatement(sql)) {
          statement.setTimestamp(1, new Timestamp(assignmentLogData.timestamp.getTime()));
          statement.setString(2, assignmentLogData.featureFlag);
          statement.setNull(3, Types.NULL); // Experiment hard coded to null for now
          statement.setString(4, assignmentLogData.variation);
          statement.setString(5, assignmentLogData.subject);
          if (assignmentLogData.subjectAttributes == null) {
            statement.setNull(6, Types.NULL);
          } else {
             // ๐Ÿ‘‡ USED HERE
            statement.setString(6, EppoAttributes.serializeNonNullAttributesToJSONString(assignmentLogData.subjectAttributes));
          }

          statement.executeUpdate();
          System.out.println("Logged assignment to bandit_test_assignments");
        } catch (SQLException e) {
          throw new RuntimeException("Unable to log assignment "+e.getMessage(), e);
        }
      })
      .banditLogger(banditLogData -> {
        String sql = "INSERT INTO bandit_test_bandit_assignments " +
          "(timestamp, experiment_name, variation_value, subject_key," +
          " action_name, action_probability, model_version," +
          " subject_numeric_attributes, subject_categorical_attributes," +
          " action_numeric_attributes, action_categorical_attributes) " +
          "SELECT ?, ?, ?, ?," +
          " ?, ?, ?," +
          " parse_json(?), parse_json(?)," +
          " parse_json(?), parse_json(?)";

        try (PreparedStatement statement = snowflakeConnection.prepareStatement(sql)) {
          statement.setTimestamp(1, new Timestamp(banditLogData.timestamp.getTime()));
          statement.setString(2, banditLogData.experiment);
          statement.setString(3, banditLogData.banditKey);
          statement.setString(4, banditLogData.subject);
          statement.setString(5, banditLogData.action);
          statement.setDouble(6, banditLogData.actionProbability);
          statement.setString(7, banditLogData.modelVersion);
          if (banditLogData.subjectNumericAttributes == null) {
            statement.setNull(8, Types.NULL);
          } else {
            // ๐Ÿ‘‡ USED HERE
            statement.setString(8, EppoAttributes.serializeNonNullAttributesToJSONString(banditLogData.subjectNumericAttributes));
          }
          if (banditLogData.subjectCategoricalAttributes == null) {
            statement.setNull(9, Types.NULL);
          } else {
            // ๐Ÿ‘‡ USED HERE
            statement.setString(9, EppoAttributes.serializeNonNullAttributesToJSONString(banditLogData.subjectCategoricalAttributes));
          }
          if (banditLogData.actionNumericAttributes == null) {
            statement.setNull(10, Types.NULL);
          } else {
            // ๐Ÿ‘‡ USED HERE
            statement.setString(10, EppoAttributes.serializeNonNullAttributesToJSONString(banditLogData.actionNumericAttributes));
          }
          if (banditLogData.actionNumericAttributes == null) {
            statement.setNull(11, Types.NULL);
          } else {
            // ๐Ÿ‘‡ USED HERE
            statement.setString(11, EppoAttributes.serializeNonNullAttributesToJSONString(banditLogData.actionCategoricalAttributes));
          }

          statement.executeUpdate();
          System.out.println("Logged bandit assignment to bandit_test_bandit_assignments");
        } catch (SQLException e) {
          throw new RuntimeException("Unable to log bandit assignment "+e.getMessage(), e);
        }
      })
      .build();
    return EppoClient.init(config);
  }

As part of this, knowing if an EppoValue is a boolean is useful for serializing, so we've added back in the EppoValue isXXX() methods.

Comment on lines +27 to +33
public static String serializeAttributesToJSONString(Map<String, ?> attributes) {
return EppoAttributes.serializeAttributesToJSONString(attributes, false);
}

public static String serializeNonNullAttributesToJSONString(Map<String, ?> attributes) {
return EppoAttributes.serializeAttributesToJSONString(attributes, true);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure yet if users will want to include or omit attributes with null values, so providing both options. (Our proxy omits them)

String serializedJSONString = eppoAttributes.serializeToJSONString();
String expectedJson = "{ \"boolean\": false, \"number\": 1.234, \"string\": \"hello\", \"null\": null }";

JSONAssert.assertEquals(expectedJson, serializedJSONString, true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONAssert() (link to library) is pretty slick. Tests for JSON equality so you don't have to worry about things like order.

Comment on lines +87 to +92
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.1</version>
<scope>test</scope>
</dependency>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test library for asserting JSON string equality (link)

@aarsilv aarsilv changed the title Add back in isXXX checks for EppoValue Convenience methods to serialize attributes to JSON string and other cleanup Mar 22, 2024
Copy link

@giorgiomartini0 giorgiomartini0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny doubt; everything else looks great

Map<String, Double> numericAttributes = new HashMap<>();
numericAttributes.put("positive", 12.3);
numericAttributes.put("negative", -45.6);
numericAttributes.put("integer", 43.0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this was intentional and I'm missing something. I would have expected 43 here, not 43.0, to check that Java ints are serialized correctly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way `Maps work, I have to provide it a double ๐Ÿ˜ž

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, true. Never mind!

@aarsilv aarsilv merged commit 1ef861b into main Mar 22, 2024
1 check passed
@aarsilv aarsilv deleted the aaron/ff-1648/improve-eppo-value branch March 22, 2024 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants