-
Notifications
You must be signed in to change notification settings - Fork 43
Saved/Cached Queries #96
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
Conversation
…o go about implementing Saved/Cached Queries and Datasets.
- In response to feedback from @Geeber in a previous PR. - KeenQueryTest was getting unwieldy. There's still plenty more to do to clean up existing tests, but at least get these sharing a common set of base functionality before adding new tests. - In preparation for adding tests for Saved/Cached Queries, Access Keys and Datasets.
- Previously there was no notion of a JSON response comprised of a top-level JSON Array, which we now need for API endpoints like getting all Saved Query definitions. Other endpoints return an array too, like listing Keen Projects in the organizational management APIs and listing Collection schemas. - For now, where we read JSON, so as to not change the public interface, we'll shove a top-level array into a special root key and return a Map anyway. Later we'll have to change the KeenJsonHandler interface to represent the fact that readJson() could return either a Map<> or a List<>. - This change only fixes the glitch for the JacksonJsonHandler, not the AndroidJsonHandler (or the TestJsonHandler), but I know how that needs to change as well, and we'll have to add tests for both. - Add a notion of using various HTTP request methods, since we are now accessing parts of the API that require GET/PUT/DELETE in addition to POST. - Understand request methods that don't/shouldn't have a body. HttpURLConnection will turn a GET into a POST if it sees a body and setDoOutput(true), which isn't what we want. Plus we likely didn't intend to try to GET with a payload, so express that in the helpers and KeenQueryClient. - KeenQueryRequest will be the place to get the knowledge of what HTTP method to use for a given request. We might later break KeenQueryRequest into two interfaces: one for representing the HTTP Request config options, and one for generating payload, since those are sort of separate concerns, and things are starting to get muddied. - Add some wrappers in KeenQueryClient so that code dispatching Query-related requests can request a List<> response or a Map<> response based on the the endpoint they're hitting, since at that layer of abstraction, calling code should know what to expect.
- KeenQueryRequest will again be the place where this configuration is housed. Normal transient queries are sent with a read key, so default to that.
…k in progress: - Add some new KeenQueryRequest descendants that represent these operations. PersistentAnalysis is, at a high level, meant to represent a Saved/Cached Query request or a Cached Dataset request in the future. - The SavedQueryRequest class specializes PersistentAnalysis for use with the Saved/Cached Queries API, and SavedQueryPut further overrides that behavior for Create/Update scenarios. - Add a Test class for this stuff, which isn't doing much testing and hits a real server, for now. This needs to be properly mocked and expanded upon. - Currently the operations that work are: Create a Saved Query, Create a Cached Query, Get all Saved/Cached Query definitions, Get a specific Saved/Cached Query definition, Delete a Saved/Cached Query. - Missing or only partially functioning are: Get a Saved/Cached Query result, Update a Saved/Cached Query definition. This gets at least a rudimentary result parsing setup working for Saved/Cached Queries: - Grab the result and look at the query definition to extract the hints our existing result parsing code needs. - Once massaged into the right shape, pass the result Map<> through the existing code to generate a QueryResult. - We could consider creating a new QueryResult for Saved/Cached Queries that also includes some of the other information, like 'run_information' and 'refresh_rate' and all that, though that information is availabe in the GetDefinition() return value too, so I'm not sure where/how to represent that. Maybe minimum it would be nice to expose a property bag with the *other* stuff in addition to the QueryResult? - Added a simple test that is designed to hit the real server...that won't get merged into 'master' as is though. Get Saved/Cached Query Update working. - This completes the basic CRUD operations for Saved/Cached Queries. - Plenty of work to do to polish this up and properly test it. - We still need to add some helpers to make things nice for the 3rd client devs.
- Not tested yet.
…f defining update maps in Java.
…y fix: - Note we now use the JSONTokener class to get the next JSON thing and then check the runtime type to see if it's JSONArray or JSONObject. - Technically this is a breaking change if any client codes implements the AndroidJsonHandler.JsonObjectManager interface since they'd have to add the newTokener() override--I'm willing to bet this isn't super common, though. If we think for some reason it *is* common, we could instead cast to AndroidJsonObjectManager and if not of that runtime type, bail in some other way that will still succeed in real code but maybe fails in test code since we can't easily mock it? That or add a completely new interface that is optionally set... - Two tests verify that both a simple JSON Object and a simple JSON Array can get parsed into Map<> and List<> respectively.
- Update is still untested. - GETting a result(s) is still untested.
Object response = getResponse(request); | ||
|
||
if (!containerType.isAssignableFrom(response.getClass())) { | ||
throw new RuntimeException(); // TODO : More specific |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deal with this TODO before merge.
} | ||
|
||
Map<String, Object> getMapResponse(KeenQueryRequest request) throws IOException { | ||
// Throw are runtime if the return type isn't as expected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deal with this TODO and same one below before merge.
|
||
private QueryResult rawMapResponseToQueryResult(KeenQueryRequest request, | ||
Map<String, Object> response) { | ||
// TODO : Eventually moving result parsing out of this class might be a good plan, like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably should just create an issue for this one, since it's going to be a large refactor.
List<Response> remaining = responses.subList(1, numExecuteCalls); | ||
Response[] responsesArray = remaining.toArray(new Response[numExecuteCalls - 1]); | ||
|
||
Response r = responses.get(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could clean this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Massive change, so hopefully I didn't miss too much! Looks like lots of goodness 🏅
((KeenDetailedCallback)callback).onSuccess(project, | ||
eventCollection, | ||
event, | ||
keenProperties); | ||
} | ||
} catch (Exception userException) { | ||
// Do nothing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize it isn't a part of this change, but I'm curious why we're silently catching these exceptions and if there's any history there, or if there is a platform-specific reason for doing this. Shouldn't we allow them to be propagate? Presumably an exception unhandled by a client really should take down the app, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good question, especially because I think this is calling client code (the success callback). If that errors, the user should know about it.
Not required to fix in this PR, if you don't want to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bummer, I typed up an entire response to this and apparently never clicked submit. Anyway the idea is a telemetry module should not take down your app since it's not crucial for event reporting to work in order for the app to work. So, failures are explicitly handled in a callback, or else they're swallowed. I think for event write, this is the proper technique, but for queries I would not do this, because one is asking for data that presumably is needed to show the user something, so failure isn't a tolerable scenario. Especially, in this case, we shouldn't re-throw because it's in the callback to client code. If client code wants to throw an exception in their callback, well, OK, I guess we could opt to never prevent you from shooting yourself in the foot, but I'd say this is a better solution in an event reporting SDK. That said, we should probably log it, so I'll make a note to add logging to these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though @josephwegner makes a good point...if the exception is raised in client code, then perhaps we really should re-throw that. I guess I could argue either side of this one. I'll file an issue and we'll hash it out there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created Issue #98.
String eventCollection, | ||
Map<String, Object> event, | ||
Map<String, Object> keenProperties) { | ||
handleSuccess(callback); | ||
if (callback != null) { | ||
try { | ||
if(callback instanceof KeenDetailedCallback){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: while you're here, there's a space missing after if
.
* functionality that requires such a key. | ||
*/ | ||
public KeenProject(String projectId, String writeKey, String readKey, String masterKey) { | ||
if (projectId == null || projectId.trim().isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we care to do any sort of key validation here? Is that done elsewhere already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What sort of validation? Since this is tossed about between modules, we can't yet know if a master is needed or a write or a read, because you may only be doing queries, or only doing administrative operations, or only doing writes, or whatever, so in theory we can't know what's needed. That said, at least one of them should be non-null and non-empty, so at a minimum we could validate that.
return mapper.readValue(reader, MAP_TYPE); | ||
// TODO : We can't assume the top-level node is a JSON Object anymore, because parts of the | ||
// API we access return a JSON Array as the root. So we need to detect the type and decide | ||
// what to return, so we need a different return type. Technically it could be a List or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wording here is confusing: "...so we need..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'll replace this TODO with either a clearer NOTE or just create an Issue, but do you have opinions on how to handle the situation in the next major revision? I'd be happy to hear what layer of abstraction you think should handle the branch between top-level object and top-level array.
* | ||
* @author masojus | ||
*/ | ||
public final class HttpMethods { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
// We expect a structure such that each entry in the list was a JSON Object representing a | ||
// query definition, and no entry should be a JSON Value. | ||
for (Object defObj : response) { | ||
if (!(defObj instanceof LinkedHashMap)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using something less specific here like Map
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was intentionally more specific only because I happen to know empirically that LinkedHashMap
is what that lib should produce. But, you're right in that we could be more lenient and future-proof if we accepted the most abstract interface that provides the necessary functionality, so I'll change it. Thanks!
|
||
requestArgs.put(KeenQueryConstants.QUERY, queryArgs); | ||
|
||
// TODO : ... or does it go here? Seems like it works both ways :S |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any resolution here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None that I've heard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@josephwegner Confirmed that it should go on the query
object as the docs state.
} | ||
|
||
// Make sure result, if provided, is wrapped in root object/array brackets, if not scalar. | ||
private String getFakeSavedQueryDefinition(String result, boolean isGroupBy, boolean isInterval, boolean isMultiAnalysis, boolean isFunnel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: long line.
analysisType = "sum"; | ||
} | ||
|
||
String definition = "{" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh man... no here string literal in Java? 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java 👎
// Should have 'refresh_rate' and 'query' top-level keys, at least. | ||
assertTrue("Missing required top-level fields.", 2 <= requestNode.size()); | ||
|
||
assertTrue(requestNode.hasNonNull(KeenQueryConstants.REFRESH_RATE)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, refresh_rate
must be specified because if it isn't the API/backend will set it to the minimum cached query interval, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, for now. It seems like a bug, but I'm not sure when they'll get to fixing it, and it's just as easy for us to pass it along.
// TODO : What should the return value be? Technically it could be a List or Map, so it | ||
// should be Object, then client code would need to do the instanceof check. For now, | ||
// so as to not break the KeenJsonHandler interface, we can stick a dummy "root" key in | ||
// the map we pass back. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anywhere in the code where this isn't passed a map? I believe our API will always respond in a map format.
Either way, I think I'm fine with creating a fake root object so it becomes a map... but I'm not terribly opinionated on the matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that the JSON parser will return either a Map<> or a List<> depending on the root JSON structure. Usually the root is a JSON Object, but now we have to handle a JSON Array as a root object too. So in the vnext_major
branch we should choose whether to branch here or at a higher layer of abstraction. This TODO is mostly an inquiry to you folks in case you have an opinion as to where that branching should occur, but it doesn't have to be decided today...and if you have no opinion then I'll just do what I think is best in the vnext_major
branch and leave this as is for the minor release.
((KeenDetailedCallback)callback).onSuccess(project, | ||
eventCollection, | ||
event, | ||
keenProperties); | ||
} | ||
} catch (Exception userException) { | ||
// Do nothing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good question, especially because I think this is calling client code (the success callback). If that errors, the user should know about it.
Not required to fix in this PR, if you don't want to.
private final Environment environment; | ||
private boolean isNetworkConnected = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it just wasn't used, but I'll make sure it has no effect.
// display name of "Can has diacritics Ñüáúéíó üöäñ çàèìòù ãõ" yields a url as follows: | ||
// "https://...?saved_query=can-has-diacritics----". So two questions remain: | ||
// 1) Can the API handle a queryName with non-ASCII? Should we allow ? | ||
// 2) This regex does not match strings like "árbol π" so we only accept ASCII |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think disallow, since the API filters them out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok that's what it does. I'll remove the comment or adjust it to reflect what and why.
|
||
// The refresh rate range empirically is [14400, 86400] seconds, inclusive at both boundaries. | ||
// A refresh rate of 0 means caching is turned off. | ||
// TODO : Docs on the website are wrong as of 3/14/17, as has been reported. Get that fixed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove this TODO? The related changes won't be in this codebase, and either what is wrong is unclear. I have a feeling this will exist forever if we commit it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this TODO will go away no matter what before merging. It's just so you and other reviewers see it and so it's in the git history, because it needs to be fixed but is external to the SDK.
public static final int MAX = 86400; // Maximum is 24 hrs | ||
|
||
public static int fromHours(int hours) { | ||
int refreshRate = hours * 3600; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we make a constant called HOUR_IN_SECONDS
, that contains 3600?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep will do.
} | ||
|
||
static void validateRefreshRate(int refreshRate) { | ||
if (0 != refreshRate && (RefreshRate.MIN > refreshRate || RefreshRate.MAX < refreshRate)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, NO_CACHING
isn't invalid is it? It just means... no caching?
KeenQueryRequest query, | ||
int refreshRate, | ||
Map<String, ?> miscProperties) { | ||
super(HttpMethods.PUT, true /* needsMasterKey */, queryName, displayName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the comment here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hate literals as actual parameters when reading code if not in the IDE. In the IDE I can just hover and see what formal parameter that maps to, but reading it elsewhere, like here in GitHub, it's so much more useful to have the comment for null
and boolean literals and number type literals, etc. It's something I always do in C and C++ programming, and if there's a good reason not to do it in Java, let me know, but my opinion is that it adds clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes I like to use enums for that reason, just another option. Sometimes that's overkill though. Named parameters would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For sure. In .NET or Python I'd just lean on named params. For null and arbitrary numerics an enum doesn't always help though. I like to either name a var or put the comment there.
Map<String, Object> rootMap = null; | ||
|
||
/* | ||
TODO : Are the calls to traverse() here any better or worse than doing either of these?: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If nobody has any insight here, I'll just leave it as is and remove the comment.
- Note that the change to throw in the KeenProject ctor could arguably be considered a breaking change. However, I don't see a realistic use case where creating a KeenProject with *just* a Project ID is useful. You can create Scoped Keys with a blank KeenClient, but you need not set its KeenProject at all, much less with just a Project ID, so I don't think this merits a major version bump.
I think I've addressed all feedback, either via code changes or by responding here inline or opening an Issue. If this all looks OK, I'll get it merged, and then I'll follow up with a PR that updates the Readme/Changelog and bumps the version to 5.2.0 in preparation for releasing the Maven artifacts. |
Fixes Issue #87 by adding support for Saved/Cached Queries.
SavedQueries
interface, but we'd want to be sure that makes sense for future work (Access Keys and Datasets, for starters) which also need Master Key for some functionality..../result
endpoints, but rather relies on pre-existingQueryResult
to represent the actual query result, and just aMap<String, Object>
to represent everything else like the Saved Query definitions. We could look at adding some helpers if it seems frequent that client code wants to access things likerun_information
and properties related to create/update info.__fake_root
stuff in a better way that requires a breaking change, but in avnext_major
branch.