1 | # coding: utf8 |
||
2 | |||
3 | """ |
||
4 | This software is licensed under the Apache 2 license, quoted below. |
||
5 | |||
6 | Copyright 2014 Crystalnix Limited |
||
7 | |||
8 | Licensed under the Apache License, Version 2.0 (the "License"); you may not |
||
9 | use this file except in compliance with the License. You may obtain a copy of |
||
10 | the License at |
||
11 | |||
12 | http://www.apache.org/licenses/LICENSE-2.0 |
||
13 | |||
14 | Unless required by applicable law or agreed to in writing, software |
||
15 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||
16 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||
17 | License for the specific language governing permissions and limitations under |
||
18 | the License. |
||
19 | """ |
||
20 | |||
21 | import os |
||
22 | |||
23 | from builtins import range |
||
24 | |||
25 | from functools import partial |
||
26 | from datetime import datetime, timedelta |
||
27 | |||
28 | from django.conf import settings |
||
29 | from django.db import transaction |
||
30 | from django.utils import timezone |
||
31 | from bitmapist import ( |
||
32 | setup_redis, mark_event, unmark_event, |
||
33 | WeekEvents, MonthEvents, DayEvents, HourEvents |
||
34 | ) |
||
35 | import pytz |
||
36 | |||
37 | from omaha.utils import ( |
||
38 | get_id, is_new_install, valuedispatch, |
||
39 | redis, get_platforms_by_appid |
||
40 | ) |
||
41 | from omaha import parser |
||
42 | from omaha.models import ( |
||
43 | ACTIVE_USERS_DICT_CHOICES, Request, AppRequest, |
||
44 | Os, Hw, Event, Version, Channel, Platform |
||
45 | ) |
||
46 | from sparkle.models import SparkleVersion |
||
47 | |||
48 | __all__ = ['userid_counting', 'is_user_active'] |
||
49 | |||
50 | setup_redis('default', |
||
51 | settings.REDIS_STAT_HOST, |
||
52 | settings.REDIS_STAT_PORT, |
||
53 | db=settings.REDIS_STAT_DB, |
||
54 | password=settings.REDIS_PASSWORD) |
||
55 | |||
56 | |||
57 | def userid_counting(userid, apps_list, platform, now=None): |
||
58 | id = get_id(userid) |
||
59 | mark_event('request', id, now=now) |
||
60 | list(map(partial(add_app_statistics, id, platform, now=now), apps_list or [])) |
||
61 | |||
62 | |||
63 | def add_app_statistics(userid, platform, app, now=None): |
||
64 | mark = partial(mark_event, now=now) |
||
65 | if not now: |
||
66 | now = timezone.now() |
||
67 | appid = app.get('appid') |
||
68 | version = app.get('version') |
||
69 | channel = parser.get_channel(app) |
||
70 | events = app.findall('event') |
||
71 | nextversion = app.get('nextversion') |
||
72 | |||
73 | err_events = filter(lambda x: x.get('eventresult') not in ['1', '2', '3'], events) |
||
74 | if err_events: |
||
75 | return |
||
76 | |||
77 | install_event = filter(lambda x: x.get('eventtype') == '2', events) |
||
78 | if is_new_install(appid, userid): |
||
79 | if install_event: |
||
80 | mark('new_install:%s' % appid, userid) |
||
81 | mark('new_install:{}:{}'.format(appid, platform), userid) |
||
82 | redis.setbit("known_users:%s" % appid, userid, 1) |
||
83 | mark('request:{}:{}'.format(appid, nextversion), userid, track_hourly=True) |
||
84 | mark('request:{}:{}:{}'.format(appid, platform, nextversion), userid, track_hourly=True) |
||
85 | mark('request:{}:{}:{}:{}'.format(appid, platform, channel, nextversion), userid, track_hourly=True) |
||
86 | mark('request:{}:{}'.format(appid, channel), userid) |
||
87 | return |
||
88 | |||
89 | elif userid not in MonthEvents('new_install:{}:{}'.format(appid, platform), year=now.year, month=now.month): |
||
90 | mark('request:%s' % appid, userid) |
||
91 | mark('request:{}:{}'.format(appid, platform), userid) |
||
92 | if nextversion: |
||
93 | mark('request:{}:{}'.format(appid, nextversion), userid, track_hourly=True) |
||
94 | mark('request:{}:{}:{}'.format(appid, platform, nextversion), userid, track_hourly=True) |
||
95 | mark('request:{}:{}:{}:{}'.format(appid, platform, channel, nextversion), userid, track_hourly=True) |
||
96 | |||
97 | uninstall_event = filter(lambda x: x.get('eventtype') == '4', events) |
||
98 | if uninstall_event: |
||
99 | mark('uninstall:%s' % appid, userid) |
||
100 | mark('uninstall:{}:{}'.format(appid, platform), userid) |
||
101 | update_event = filter(lambda x: x.get('eventtype') == '3', events) |
||
102 | if update_event: |
||
103 | unmark_event('request:{}:{}'.format(appid, version), userid, track_hourly=True) |
||
104 | unmark_event('request:{}:{}:{}'.format(appid, platform, version), userid, track_hourly=True) |
||
105 | unmark_event('request:{}:{}:{}:{}'.format(appid, platform, channel, version), userid, track_hourly=True) |
||
106 | mark('request:{}:{}'.format(appid, nextversion), userid, track_hourly=True) |
||
107 | mark('request:{}:{}:{}'.format(appid, platform, nextversion), userid, track_hourly=True) |
||
108 | mark('request:{}:{}:{}:{}'.format(appid, platform, channel, nextversion), userid, track_hourly=True) |
||
109 | else: |
||
110 | mark('request:{}:{}'.format(appid, version), userid, track_hourly=True) |
||
111 | mark('request:{}:{}:{}'.format(appid, platform, version), userid, track_hourly=True) |
||
112 | mark('request:{}:{}:{}:{}'.format(appid, platform, channel, version), userid, track_hourly=True) |
||
113 | mark('request:{}:{}'.format(appid, channel), userid) |
||
114 | |||
115 | |||
116 | def get_users_statistics_months(app_id, platform=None, year=None, start=1, end=12): |
||
117 | now = timezone.now() |
||
118 | if not year: |
||
119 | year = now.year |
||
120 | |||
121 | if platform: |
||
122 | install_event_name = 'new_install:{}:{}'.format(app_id, platform) |
||
123 | update_event_name = 'request:{}:{}'.format(app_id, platform) |
||
124 | uninstall_event_name = 'uninstall:{}:{}'.format(app_id, platform) |
||
125 | else: |
||
126 | install_event_name = 'new_install:%s' % app_id |
||
127 | update_event_name = 'request:%s' % app_id |
||
128 | uninstall_event_name = 'uninstall:%s' % app_id |
||
129 | |||
130 | installs_by_month = [] |
||
131 | updates_by_month = [] |
||
132 | uninstalls_by_month = [] |
||
133 | for m in range(start, end + 1): |
||
134 | installs_by_month.append(MonthEvents(install_event_name, year, m)) |
||
135 | updates_by_month.append(MonthEvents(update_event_name, year, m)) |
||
136 | uninstalls_by_month.append(MonthEvents(uninstall_event_name, year, m)) |
||
137 | installs_data = [(datetime(year, start + i, 1).strftime("%Y-%m"), len(e)) for i, e in enumerate(installs_by_month)] |
||
138 | updates_data = [(datetime(year, start + i, 1).strftime("%Y-%m"), len(e)) for i, e in enumerate(updates_by_month)] |
||
139 | res = dict(new=installs_data, updates=updates_data) |
||
140 | if platform != 'mac': |
||
141 | uninstalls_data = [(datetime(year, start + i, 1).strftime("%Y-%m"), len(e)) for i, e in enumerate(uninstalls_by_month)] |
||
142 | res.update(dict(uninstalls=uninstalls_data)) |
||
143 | return res |
||
144 | |||
145 | |||
146 | def get_users_statistics_weeks(app_id=None): |
||
147 | now = timezone.now() |
||
148 | event_name = 'request:%s' % app_id if app_id else 'request' |
||
149 | year = now.year |
||
150 | current_week = now.isocalendar()[1] |
||
151 | previous_week = (now - timedelta(weeks=1)).isocalendar()[1] |
||
152 | yesterday = now - timedelta(days=1) |
||
153 | data = [ |
||
154 | ('Previous week', len(WeekEvents(event_name, year, previous_week))), |
||
155 | ('Current week', len(WeekEvents(event_name, year, current_week))), |
||
156 | ('Yesterday', len(DayEvents(event_name, year, yesterday.month, yesterday.day))), |
||
157 | ('Today', len(DayEvents(event_name, year, now.month, now.day))), |
||
158 | ] |
||
159 | return data |
||
160 | |||
161 | |||
162 | def get_channel_statistics(app_id, date=None): |
||
163 | if not date: |
||
164 | date = timezone.now() |
||
165 | |||
166 | event_name = 'request:{}:{}' |
||
167 | channels = [c.name for c in Channel.objects.all()] |
||
168 | data = [(channel, len(MonthEvents(event_name.format(app_id, channel), date.year, date.month))) for channel in channels] |
||
169 | data = filter(lambda x: x[1], data) |
||
170 | return data |
||
171 | |||
172 | |||
173 | def get_users_versions_by_platform(app_id, platform, date): |
||
174 | if platform == 'mac': |
||
175 | versions = [str(v) for v in SparkleVersion.objects.filter_by_enabled(app_id=app_id).values_list('short_version', flat=True)] |
||
176 | else: |
||
177 | versions = [str(v) for v in Version.objects.filter_by_enabled(app__id=app_id, platform__name=platform).values_list('version', flat=True)] |
||
178 | event_name = 'request:{}:{}:{}' |
||
179 | data = [(v, len(MonthEvents(event_name.format(app_id, platform, v), date.year, date.month))) for v in versions] |
||
180 | data = filter(lambda x: x[1], data) |
||
181 | return dict(data) |
||
182 | |||
183 | |||
184 | def get_users_versions(app_id, date=None): |
||
185 | if not date: |
||
186 | date = timezone.now() |
||
187 | |||
188 | platforms = Platform.objects.values_list('name', flat=True) |
||
189 | data = dict() # try to move it in the separate function |
||
190 | for platform in platforms: |
||
191 | platform_data = get_users_versions_by_platform(app_id, platform, date) |
||
192 | data.update({platform: platform_data}) |
||
193 | |||
194 | return data |
||
195 | |||
196 | |||
197 | |||
198 | View Code Duplication | def get_hourly_data_by_platform(app_id, end, n_hours, versions, platform, channel, tz='UTC'): |
|
0 ignored issues
–
show
Duplication
introduced
by
![]() |
|||
199 | def build_event_name(app_id, platform, channel, v): |
||
200 | if channel: |
||
201 | return "request:{}:{}:{}:{}".format(app_id, platform, channel, v) |
||
202 | else: |
||
203 | return "request:{}:{}:{}".format(app_id, platform, v) |
||
204 | |||
205 | tzinfo = pytz.timezone(tz) |
||
206 | start = end - timezone.timedelta(hours=n_hours) |
||
207 | |||
208 | hours = [datetime(start.year, start.month, start.day, start.hour, tzinfo=pytz.UTC) |
||
209 | + timezone.timedelta(hours=x) for x in range(1, n_hours + 1)] |
||
210 | |||
211 | data = [(v, [[hour.astimezone(tzinfo).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), |
||
212 | len(HourEvents.from_date(build_event_name(app_id, platform, channel, v), hour))] |
||
213 | for hour in hours]) |
||
214 | for v in versions] |
||
215 | data = filter(lambda version_data: sum([data[1] for data in version_data[1]]), data) |
||
216 | return dict(data) |
||
217 | |||
218 | |||
219 | View Code Duplication | def get_daily_data_by_platform(app_id, end, n_days, versions, platform, channel): |
|
0 ignored issues
–
show
|
|||
220 | def build_event_name(app_id, platform, channel, v): |
||
221 | if channel: |
||
222 | return "request:{}:{}:{}:{}".format(app_id, platform, channel, v) |
||
223 | else: |
||
224 | return "request:{}:{}:{}".format(app_id, platform, v) |
||
225 | |||
226 | start = end - timezone.timedelta(days=n_days) |
||
227 | |||
228 | days = [start + timezone.timedelta(days=x) for x in range(0, n_days+1)] |
||
229 | data = [(v, [[day.strftime("%Y-%m-%dT00:%M:%S.%fZ"), |
||
230 | len(DayEvents.from_date(build_event_name(app_id, platform, channel, v), day))] |
||
231 | for day in days]) |
||
232 | for v in versions] |
||
233 | data = filter(lambda version_data: sum([data[1] for data in version_data[1]]), data) |
||
234 | return dict(data) |
||
235 | |||
236 | |||
237 | def get_users_live_versions(app_id, start, end, channel, tz='UTC'): |
||
238 | import logging |
||
239 | logging.info("Getting active versions from DB") |
||
240 | platforms = [x.name for x in get_platforms_by_appid(app_id)] |
||
241 | versions = {} |
||
242 | |||
243 | for platform in platforms: |
||
244 | if platform == 'mac': |
||
245 | platform_data = [str(v) for v in SparkleVersion.objects.filter(app_id=app_id).values_list('short_version', flat=True)] |
||
246 | else: |
||
247 | platform_data = [str(v) for v in Version.objects.filter(app__id=app_id, platform__name=platform).values_list('version', flat=True)] |
||
248 | versions.update({platform: platform_data}) |
||
249 | |||
250 | logging.info("Getting statistics from Redis") |
||
251 | if start < timezone.now() - timedelta(days=7): |
||
252 | n_days = (end-start).days |
||
253 | |||
254 | data = dict() |
||
255 | for platform in platforms: |
||
256 | platform_data = get_daily_data_by_platform(app_id, end, n_days, versions[platform], platform, channel) |
||
257 | data.update({platform: platform_data}) |
||
258 | |||
259 | else: |
||
260 | tmp_hours = divmod((end - start).total_seconds(), 60 * 60) |
||
261 | n_hours = tmp_hours[0] + 1 |
||
262 | n_hours = int(n_hours) |
||
263 | |||
264 | data = dict() |
||
265 | for platform in platforms: |
||
266 | platform_data = get_hourly_data_by_platform(app_id, end, n_hours, versions[platform], platform, channel, tz=tz) |
||
267 | data.update({platform: platform_data}) |
||
268 | return data |
||
269 | |||
270 | |||
271 | @valuedispatch |
||
272 | def is_user_active(period, userid): |
||
273 | return False |
||
274 | |||
275 | |||
276 | @is_user_active.register(ACTIVE_USERS_DICT_CHOICES['all']) |
||
277 | def _(period, userid): |
||
278 | return True |
||
279 | |||
280 | |||
281 | @is_user_active.register(ACTIVE_USERS_DICT_CHOICES['week']) |
||
282 | def _(period, userid): |
||
283 | return get_id(userid) in WeekEvents.from_date('request', timezone.now()) |
||
284 | |||
285 | |||
286 | @is_user_active.register(ACTIVE_USERS_DICT_CHOICES['month']) |
||
287 | def _(period, userid): |
||
288 | return get_id(userid) in MonthEvents.from_date('request', timezone.now()) |
||
289 | |||
290 | |||
291 | def get_kwargs_for_model(cls, obj, exclude=None): |
||
292 | exclude = exclude or [] |
||
293 | fields = [(field.name, field.to_python) for field in cls._meta.fields if field.name not in exclude] |
||
294 | kwargs = dict([(i, convert(obj.get(i))) for (i, convert) in fields]) |
||
295 | return kwargs |
||
296 | |||
297 | |||
298 | def parse_os(os): |
||
299 | kwargs = get_kwargs_for_model(Os, os, exclude=['id']) |
||
300 | obj, flag = Os.objects.get_or_create(**kwargs) |
||
301 | return obj |
||
302 | |||
303 | |||
304 | def parse_hw(hw): |
||
305 | kwargs = get_kwargs_for_model(Hw, hw, exclude=['id']) |
||
306 | obj, flag = Hw.objects.get_or_create(**kwargs) |
||
307 | return obj |
||
308 | |||
309 | |||
310 | def parse_req(request, ip=None): |
||
311 | kwargs = get_kwargs_for_model(Request, request, exclude=['os', 'hw', 'created', 'id']) |
||
312 | kwargs['ip'] = ip |
||
313 | return Request(**kwargs) |
||
314 | |||
315 | |||
316 | def parse_apps(apps, request): |
||
317 | app_list = [] |
||
318 | for app in apps: |
||
319 | events = app.findall('event') |
||
320 | |||
321 | if not events: |
||
322 | continue |
||
323 | |||
324 | kwargs = get_kwargs_for_model(AppRequest, app, exclude=['request', 'version', 'nextversion', 'id']) |
||
325 | kwargs['version'] = app.get('version') or None |
||
326 | kwargs['nextversion'] = app.get('nextversion') or None |
||
327 | kwargs['tag'] = parser.get_channel(app) |
||
328 | app_req = AppRequest.objects.create(request=request, **kwargs) |
||
329 | event_list = parse_events(events) |
||
330 | app_req.events.add(*event_list) |
||
331 | app_list.append(app_req) |
||
332 | return app_list |
||
333 | |||
334 | |||
335 | def parse_events(events): |
||
336 | res = [] |
||
337 | for event in events: |
||
338 | kwargs = get_kwargs_for_model(Event, event) |
||
339 | res.append(Event.objects.create(**kwargs)) |
||
340 | return res |
||
341 | |||
342 | |||
343 | @transaction.atomic |
||
344 | def collect_statistics(request, ip=None): |
||
345 | userid = request.get('userid') |
||
346 | apps = request.findall('app') |
||
347 | |||
348 | if userid: |
||
349 | userid_counting(userid, apps, request.os.get('platform')) |
||
350 | |||
351 | if not filter(lambda app: bool(app.findall('event')), apps): |
||
352 | return |
||
353 | |||
354 | req = parse_req(request, ip) |
||
355 | req.os = parse_os(request.os) |
||
356 | req.hw = parse_hw(request.hw) if request.get('hw') else None |
||
357 | req.save() |
||
358 | |||
359 | parse_apps(apps, req) |
||
360 |