1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
17 # Easy and ready to use web tracker you can apply to your popcorn application.
19 # The only thing you have to do is to use the tracker in your app routes:
22 # var config = new AppConfig
24 # app.use("/", new HomeHandler)
25 # app.use("/products", new ProductsHandler)
26 # app.use("customers/", new CustomersHandler)
28 # app.use_after("/*", new PopTracker(config)) # tracker listens to /*
31 # You can also use multiple tracker at once on different route.
32 # All the data will be aggregated for you.
35 # app.use_after("/api/*", new PopTracker(config))
36 # app.use_after("/admin/*", new PopTracker(config))
39 # To retrieve your tracker data use the `PopTrackerAPI` which serves the tracker
40 # data in JSON format.
43 # app.use("/api/tracker_data", new PopTrackerAPI(config))
48 import popcorn
::pop_config
49 import popcorn
::pop_logging
50 import popcorn
::pop_json
51 import popcorn
::pop_repos
56 var tracker_logs
= new TrackerRepo(db
.collection
("tracker_logs"))
59 var tracker
= new PopTracker(self)
62 # JSON API of the PopTracker
64 # Serves the collected data in JSON format.
68 # Config used to access tracker db
72 use
("/entries", new PopTrackerEntries(config
))
73 use
("/queries", new PopTrackerQueries(config
))
74 use
("/browsers", new PopTrackerBrowsers(config
))
75 use
("/times", new PopTrackerResponseTime(config
))
79 # Base tracker handler
80 abstract class TrackerHandler
83 # Config used to access tracker db
86 # Get the `limit` GET argument from `req`
88 # Return `10` by default.
89 fun limit
(req
: HttpRequest): Int do return req
.int_arg
("limit") or else 10
92 # Saves logs into a MongoDB collection
97 redef fun all
(req
, res
) do
98 config
.tracker_logs
.save
new LogEntry(req
, res
)
102 # List all tracker log entries
103 class PopTrackerEntries
106 redef fun get
(req
, res
) do
107 res
.json
new JsonArray.from
(config
.tracker_logs
.find_all
)
111 # Group and count entries by query string
112 class PopTrackerQueries
115 redef fun get
(req
, res
) do
116 var pipe
= new MongoPipeline
117 pipe
.group
((new MongoGroup("$request.uri")).
119 avg
("response_time", "$response_time").
120 addToSet
("uniq", "$session"))
121 pipe
.sort
((new MongoMatch).eq
("visits", -1))
122 pipe
.limit
(limit
(req
))
123 res
.json
new JsonArray.from
(config
.tracker_logs
.collection
.aggregate
(pipe
))
127 # Group and count entries by browser
128 class PopTrackerBrowsers
131 # MongoMatch query used for each browser key
133 # Because parsing user agent string is a pain in the nit, we go lazy on this
134 # one. We associate each broswer key like `Chromium` to the query that allows
135 # us to count the number of visits.
136 var browser_queries
: HashMap[String, MongoMatch] do
137 var map
= new HashMap[String, MongoMatch]
138 map
["Chromium"] = (new MongoMatch).regex
("user_agent", "Chromium")
139 map
["Edge"] = (new MongoMatch).regex
("user_agent", "Edge")
140 map
["Firefox"] = (new MongoMatch).regex
("user_agent", "Firefox")
141 map
["IE"] = (new MongoMatch).regex
("user_agent", "(MSIE)|(Trident)")
142 map
["Netscape"] = (new MongoMatch).regex
("user_agent", "Netscape")
143 map
["Opera"] = (new MongoMatch).regex
("user_agent", "Opera")
144 map
["Safari"] = (new MongoMatch).land
(null, [
145 (new MongoMatch).regex
("user_agent", "Safari"),
146 (new MongoMatch).regex
("user_agent", "^((?!Chrome).)*$")])
147 map
["Chrome"] = (new MongoMatch).land
(null, [
148 (new MongoMatch).regex
("user_agent", "Chrome"),
149 (new MongoMatch).regex
("user_agent", "^((?!Edge).)*$")])
154 # Apply the `query` on `TrackerRepo::count`
155 fun browser_count
(query
: MongoMatch): Int do return config
.tracker_logs
.count
(query
)
157 redef fun get
(req
, res
) do
158 var browsers
= new Array[BrowserCount]
159 for browser
, query
in self.browser_queries
do
160 var count
= new BrowserCount(browser
, browser_count
(query
))
161 if count
.count
> 0 then browsers
.add count
164 for browser
in browsers
do sum
+= browser
.count
165 var other
= config
.tracker_logs
.count
- sum
166 if other
> 0 then browsers
.add
new BrowserCount("Other", other
)
167 default_comparator
.sort
(browsers
)
168 res
.json
new JsonArray.from
(browsers
)
172 # Associate each browser to its count.
174 # Only used to serialize the results.
175 private class BrowserCount
180 redef type OTHER: SELF
185 redef fun <=>(o
) do return o
.count
<=> count
188 # Return last month response time
189 class PopTrackerResponseTime
192 redef fun get
(req
, res
) do
193 var limit
= get_time
- (3600 * 24 * 30)
194 var pipe
= new MongoPipeline
195 pipe
.match
((new MongoMatch).gte
("timestamp", limit
))
196 pipe
.group
((new MongoGroup("$timestamp")).
198 avg
("response_time", "$response_time"))
199 pipe
.sort
((new MongoMatch).eq
("_id", -1))
200 res
.json
new JsonArray.from
(config
.tracker_logs
.collection
.aggregate
(pipe
))
204 # A tracker log entry used to store HTTP requests and their given HTTP responses
209 # HTTP request that triggered that log entry
210 var request
: HttpRequest
212 # HTTP response returned by the serveur
213 var response
: HttpResponse
215 # Request user-agent shortcut (easier for db requests
216 var user_agent
: nullable String is lazy
do return request
.header
["User-Agent"]
218 # Processing time in miliseconds (or null if no clock was found in request)
219 var response_time
: nullable Int is lazy
do
220 var clock
= request
.clock
221 if clock
== null then return null
222 return (clock
.total
* 1000.0).to_i
225 # Log entry timestamp
226 var timestamp
: Int = get_time
228 # Session ID associated to this entry
229 var session
: nullable String is lazy
do
230 var session
= request
.session
231 if session
== null then return null
232 return session
.id_hash
236 # Repository to store track logs.
238 super MongoRepository[LogEntry]