Not 100% sure what route this should take.
Some questions that come to mind:
- How do we handle data that we currently collect that can't be collected on the client side? (e.g. peak memory, timer, etc.)
- How do we handle client-side asynchronous dependencies, if at all? (e.g. logging a value asynchronously retrieved on the client side from an API)
- Is it worth creating a client side API for this?
Comments
Comment #1
mondrakeHi,
out of curiosity, how are you thinking to go about this? In D8 where the access log has been dropped, now a node counter remains - each page served contains a small js that runs on the client and posts an ajax call - which in turns reenters in Drupal and increments the node views counter. Will you use the same mechanism?
Comment #2
iamEAP commentedI think the general idea will be the same, though in the spirit of the module, it will be easily overridden. Perhaps something like the following:
Comment #3
mondrakeAll right then, just sharing some free thoughts from my side
a) this method will only work for http served pages, and only for clients that have js enabled
b) other requests (e.g. download of private files) will still have to be processed synchronously through the hook_exit implementation
c) this means that better statistics will be in the position to potentially collect field values in different scenarios:
One idea could be to have a scope control variable to be used in better_statistics_get_fields_data() to specify the context in which the collection is taking place, to be passed on to the field callbacks, and adopt a rule that only fields that have not been collected already should be collected in current scope call.
This may help on your point
The idea could be the following, for the case of an asynchronous access log
Field callback implementations should be aware of the scope, to avoid firing when not (yet) needed.
On cached pages, though, this may not work (e.g. what would be the purpose of peak memory and timer there...).
Comment #4
iamEAP commentedDefinitely appreciate the thoughts, mondrake.
Not sure I'm 100% sold on a scope control variable, but I definitely agree more context could be provided in that hook invocation. Unfortunately, adding it would break the API.
I think the primary use case that I'd like to solve is related to what you were alluding to at the end there: allowing some level of access statistics collection for statically cached pages (e.g. Varnish, boost, etc). That being said, I think we can't only support pages rendered by JS-enabled clients; it would largely defeat one of the use-cases for the Core Statistics module (e.g. easily finding malicious bots' IPs for blocking). Maybe a compromise would be allowing the module to run in three modes:
High level architecture for the async callback:
High level architecture for the JS API:
Comment #5
iamEAP commentedNowhere near done, but getting there. Taking a long time because I am definitely not a frontend developer. Attaching the patch to get a feel for what TestBot thinks. No new tests yet.
Done so far
statistics_enable_access_logvariable to be on/off + the modes I described above (client-side only and mixed mode),bs(variable name is technically configurable),bs('accesslog');bs('entityView', 'node', 1);BetterStatistics.prototype.accesslog = function() {/* my new function */};BetterStatistics.prototype.newMethod = function() {/* my brand new method */};, which would be invoked like so:bs('newMethod');Still todo
hook_better_statistics_fields()implementors,Comment #6
iamEAP commentedComment #7
iamEAP commentedTests for the additional server-side functionality from #5. If they pass, I'll start hammering on the todos.
Comment #9
iamEAP commentedTests were passing, but there were exceptions due to calling system/ajax via GET and with no params. I found another instance of core that didn't return HTML and swapped it in place (rss.xml). Also added assertions on HTML-based pages that ensure the API JS is included, as well as the queue stub.
Comment #10
iamEAP commentedWorking on this during DrupalCon Portland Sprints.
Comment #11
iamEAP commentedAnother pile of work as well as tests to ensure the AJAX callback handles JSON data correctly.
Done in this chunk
hook_better_statistics_fields()implementors. This is done with a new 'js' key on the returned array which is itself an array with at least 'data' and 'type' keys, but can include any other option passed todrupal_add_js()./statistics/ajax/TYPE, where TYPE is the type of statistical data being collected,$(document).bind('statistics.accesslog', function(e, data) {...});,Still todo
hook_better_statistics_fields(),Comment #12
iamEAP commentedMore progress; discovered a bug where cached pages would get 2 accesslog hits because hook_exit() had no idea that JS existed on the page (which would not occur w/Varnish or Boost). Added some code in prelog to prevent that situation (as well as a test). Still needs some more tests work
Done in this chunk
hook_better_statistics_fields(), added it to the list of keys able to be written through on cache flush / configuration change / cron,Still todo
js/fields/custom.js),Comment #13
iamEAP commentedPer the remaining todos, I'm moving this back to needs work. That being said, if patches had release candidates, #12 would be the first.
Comment #14
iamEAP commentedGave up on the cache status from client-side issue. Don't believe it's possible to solve. Also removed the need for an update hook by very slightly refactoring the field update function.
Request for API Feedback
After looking at the code, I've discovered there's actually very little in the way of dependencies, save for the jQuery event triggering/binding. Wondering from an API perspective whether or not we should kill off that jQuery dependency and write a short, standard JavaScript event triggering method for BetterStatistics. The difference for JS developers would be...
Vs.
On the one hand, this makes it so that it's totally portable and can be run on page with 0 dependencies. On the other hand, it's very likely that anyone doing advanced Stats/Analytics implementations, especially on a Drupal install, is likely to already be using jQuery. I tend to lean toward the former. Would love to hear feedback from anyone else listening in on this thread.
Done in this chunk
_better_statistics_update_fields()that allow changes to the Core Statistics/default fields to be written through (and therefore, removes the need for yet another update hook).Still todo
Comment #16
iamEAP commented#14: better_statistics-collect_stats_client_side-1960704-14.patch queued for re-testing.
Comment #18
iamEAP commented#14: better_statistics-collect_stats_client_side-1960704-14.patch queued for re-testing.
Comment #19
iamEAP commentedStill an opportunity to change it, but I decided to go with the non-jQuery trigger for maximum portability.
All that's left is to add tests for the entity_view.
Comment #20
iamEAP commentedAdded tests for
bs('entity_view')'s callback. Consider this patch a solid release candidate.Looking for any and all feedback (on the API, the functionality, etc). A quick recap:
To add a JS-implementation of an existing Better Statistics Field, add this to your
hook_better_statistics_fields():Which will auto-include your my_field.js file when needed. In that file, you would do something like this to register your data as it is determined on the client-side:
Plenty of other goodies and ways to extend Better Statistics, but I'll save those for a doc page (the module is complex enough to warrant one now).
Comment #22
iamEAP commented#20: better_statistics-collect_stats_client_side-1960704-20.patch queued for re-testing.
Comment #24
iamEAP commented#20: better_statistics-collect_stats_client_side-1960704-20.patch queued for re-testing.
Comment #26
iamEAP commented#20: better_statistics-collect_stats_client_side-1960704-20.patch queued for re-testing.
Comment #27
iamEAP commentedMaking better_statistics.js a Library entry via hook_library() to simplify inclusion in other contexts. Also making the bs JS global truly swappable.
Comment #28
iamEAP commentedMean to attach the interdiff the second time...
Comment #30
iamEAP commented#27: better_statistics-collect_stats_client_side-1960704-27.patch queued for re-testing.
Comment #31
mondrakeHi @iamEAP
just started having a look - stellar work!!
One glitch
in better_statistics.js, line 64 you have
this won't work if your DRUPAL_ROOT does not coincide with ... the root directory. I just changed the js file on the fly and it works, but I believe Drupal.settings.basePath could be a more permanent solution
See this post
Comment #32
mondrakeYes, the following change in line 64 of better_statistics.js works, also locally simpletest works fine
EDIT 1:
However, now I started getting 'page not found' messages in the watchdog for page undefinedstatistics/ajax/accesslog ... strange, I will look more into it
EDIT 2:
I added a dependency in the hook_library implementation, and now no more watchdog messages
However, I have problems on running tests locally (with no apparent pattern, so I am not sure I can relate them to this)
Comment #33
mondrakeOK, so an update with patch and interdiff to make things clearer.
I was having a problem with the fact that my test environment has a base_path different than '/'. It is necessary to specify it to have a fully qualified ajax callback path. Adding 'Drupal.settings.basePath' as per #32 works to an extent, but sometimes is failing as the sequence of js processing is such that the bs scripts are executing 'before' the client is resolving Drupal.settings.basePath (hence the error reported in edit 1).
To cut short, I propose to pass the basePath as a separate property of the bs instance like patch attached. Here we can be sure that the info is available at the time of the ajax post. Also, in D8 perspective where all js is optional, this could help in keeping bs footprint to a minimum in case e.g. of cached pages, avoiding dependency on other scripts.
Solution in edit 2 is not working, and in any case would contradict the concept of this patch.
Hope it helps... more questions will come.
Cheers
Comment #34
mondrakeComment #35
iamEAP commentedGood catch, mondrake. And excellent usage of the API in #33! It's fundamental enough that I might want to add it in the hook_library instantiation, though. e.g.:
I found a more fundamental flaw with the API, though...
I started abstracting accesslog and entity_view (and their prototype definitions) into separate files and separate hook_library entries so you could run
drupal_add_library('better_statistics', 'bsjsapi-accesslog')anddrupal_add_library('better_statistics', 'bsjsapi-entity_view')independently and have it resolve the dependency for you).The problem is that due to the async call to better_statistics.js, it's possible that the BetterStatistics global function/object is undefined when one of these files (e.g. better_statistics.accesslog.js) attempts to define a prototype method on BetterStatistics. In many (I've found most) cases, the async call hasn't finished processing yet, so there's an error (like "can't define prototype.accesslog of undefined" or something).
Relatedly, there's little stopping someone from firing bs('custom_callback') between when it runs through all queued requests and when BetterStatistics.prototype.custom_callback is defined.
As such, it's nearly impossible to define useful custom callbacks (or override existing callbacks).
Comment #36
iamEAP commentedTons of changes here, building off of #33 and attempting to solve the problems from #35. I'll post an interdiff shortly to make things clear (getting very messy with thousand line patches)...
Done
better_statistics_add_method(), with a file to your method's JS implementation. That function statically holds on to all added methods. On every page, Better Statistics will attempt to load all added methods. If methods have been added, it will take them and wrap them between better_statistics.init.js and better_statistics.exec.js (where the BetterStatistics.prototype.accesslog, etc. definitions existed previously), and build a cached JS file as Drupal normally does when JS preprocessing is turned on. This file is then used as the BS JS API file. Convoluted, but at least has an easy-to-understand interface for developers; also, only the methods that are needed on a given page are added (this may not be desirable, now that I think about it; see the first "to be done" item below).To be done
Comment #38
iamEAP commentedInterdiff of #36, for reference.
Comment #39
iamEAP commentedDone
To be done
Comment #40
iamEAP commentedOkay. Cleaning up uninstall and tests. Here's another "release candidate" patch.
Comment #41
iamEAP commentedFrom a browser that doesn't implement Navigation Timing:
ReferenceError: Can't find variable: performanceComment #42
iamEAP commentedHow about...
window.performance = window.performance || {};Comment #43
mondrakeHi, I've been running this on my dev for a while in different modes, and it works a treat. It's RTBC for me.
Just a couple of things, for your consideration.
1. I *think I* understand the various options for the operation modes 'Server-side' 'Client-side' 'mixed mode', but believe that it would be difficult for users to pick. I wonder whether there could be a more intuitive way to present this in the admin UI.
2. It's a pity that client mode would not fit with capturing the cache status. I understand that that info is in the HTTP headers but not accessible to js. I googled a bit and found something that lit a bulb. How about passing that info from the server side in a cookie. That would be available to js in
document.cookie. hook_boot is a possible place where to capture the HIT status (well, in D7 at least...). Something like this (very very rough):Comment #44
iamEAP commentedThanks, mondrake.
re #1: Totally agree. Maybe something like the following?
Where the above is true if we assume "to Drupal" means "to the Drupal codebase and not a reverse proxy cache or a statically cached HTML page."
You might also think of it in terms of capabilities, but these would be unsuitable for a checkbox label. Maybe as description text below?
I think in a 2.x version of the API, I'll want to break off page views from the accesslog as its own API method.
re #2: An interesting thought, but I think it poses a few problems. Namely, as soon as the cookie is set, I believe strange things will start happening with caching (since Drupal varies on cookie). Additionally, this assumes the request makes it back to Drupal; what about Varnish/Boost/etc?
Comment #45
mondrakere #1 how about a mix:
re #2 I do not have experience with these tools, but of course if the request does not get to Drupal then there seems to be no space. I was just looking at how _drupal_bootstrap_page_cache() is working and believed that there is space to add a cookie before the cached paged response gets built.
Comment #46
iamEAP commentedMight be acceptable. How about this?
As per #2, since I would very much like it, how about we open up a follow-up / feature request to try and get it working?
Comment #47
mondrakeFollow up for #2 makes sense.
Thanks
Comment #48
iamEAP commentedExcellent! As always, thanks very much for your feedback and reviews, mondrake!
Committed 1603dac.
Follow-up created here: #2019937: Detect cache status from the client-side