Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
event-tracking
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
event-tracking
Commits
64a93514
Commit
64a93514
authored
Oct 21, 2013
by
Gabe Mulley
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #26 from mulby/gabe/processors
support event processors
parents
35691e74
420dba33
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
180 additions
and
25 deletions
+180
-25
eventtracking/django/__init__.py
+48
-22
eventtracking/django/tests/test_configuration.py
+24
-0
eventtracking/tests/test_track.py
+72
-0
eventtracking/tracker.py
+36
-3
No files found.
eventtracking/django/__init__.py
View file @
64a93514
...
...
@@ -11,7 +11,8 @@ from eventtracking.tracker import Tracker
from
eventtracking.locator
import
ThreadLocalContextLocator
DJANGO_SETTING_NAME
=
'TRACKING_BACKENDS'
DJANGO_BACKEND_SETTING_NAME
=
'TRACKING_BACKENDS'
DJANGO_PROCESSOR_SETTING_NAME
=
'TRACKING_PROCESSORS'
class
DjangoTracker
(
Tracker
):
...
...
@@ -22,13 +23,13 @@ class DjangoTracker(Tracker):
def
__init__
(
self
):
backends
=
self
.
create_backends_from_settings
()
super
(
DjangoTracker
,
self
)
.
__init__
(
backends
,
ThreadLocalContextLocator
())
processors
=
self
.
create_processors_from_settings
()
super
(
DjangoTracker
,
self
)
.
__init__
(
backends
,
ThreadLocalContextLocator
(),
processors
)
def
create_backends_from_settings
(
self
):
"""
Expects the Django setting `setting_name` (defaults to
"TRACKING_BACKENDS") to be defined and point to a dictionary of
backend engine configurations.
Expects the Django setting "TRACKING_BACKENDS" to be defined and point
to a dictionary of backend engine configurations.
Example::
...
...
@@ -47,46 +48,71 @@ class DjangoTracker(Tracker):
},
}
"""
config
=
getattr
(
settings
,
DJANGO_SETTING_NAME
,
{})
config
=
getattr
(
settings
,
DJANGO_
BACKEND_
SETTING_NAME
,
{})
backends
=
{}
for
name
,
values
in
config
.
iteritems
():
# Ignore empty values to turn-off default tracker backends
if
values
and
'ENGINE'
in
values
:
engine
=
values
[
'ENGINE'
]
options
=
values
.
get
(
'OPTIONS'
,
{})
backend
=
self
.
instantiate_backend_from_name
(
engine
,
options
)
backend
=
self
.
instantiate_from_dict
(
values
)
backends
[
name
]
=
backend
return
backends
def
instantiate_
backend_from_name
(
self
,
name
,
option
s
):
def
instantiate_
from_dict
(
self
,
value
s
):
"""
Instantiate an event tracker backend from the full module path to
the backend class. Useful when setting backends from configuration
files.
Constructs an object given a dictionary containing an "ENGINE" key
which contains the full module path to the class, and an "OPTIONS"
key which contains a dictionary that will be passed in to the
constructor as keyword args.
"""
# Parse backend name
name
=
values
[
'ENGINE'
]
options
=
values
.
get
(
'OPTIONS'
,
{})
# Parse the name
parts
=
name
.
split
(
'.'
)
module_name
=
'.'
.
join
(
parts
[:
-
1
])
class_name
=
parts
[
-
1
]
# Get and verify the backend class
# Get the class
try
:
module
=
import_module
(
module_name
)
cls
=
getattr
(
module
,
class_name
)
except
(
ValueError
,
AttributeError
,
TypeError
,
ImportError
):
raise
ValueError
(
'Cannot find
event tracker backend
%
s'
%
name
)
raise
ValueError
(
'Cannot find
class
%
s'
%
name
)
backend
=
cls
(
**
options
)
if
not
hasattr
(
backend
,
'send'
)
or
not
callable
(
backend
.
send
):
raise
ValueError
(
'Backend
%
s does not have a callable "send" method.'
%
name
)
return
cls
(
**
options
)
def
create_processors_from_settings
(
self
):
"""
Expects the Django setting "TRACKING_PROCESSORS" to be defined and
point to a list of backend engine configurations.
Example::
TRACKING_PROCESSORS = [
{
'ENGINE': 'some.arbitrary.Processor'
},
{
'ENGINE': 'some.arbitrary.OtherProcessor',
'OPTIONS': {
'user': 'foo'
}
},
]
"""
config
=
getattr
(
settings
,
DJANGO_PROCESSOR_SETTING_NAME
,
[])
processors
=
[]
for
values
in
config
:
# Ignore empty values to turn-off default tracker backends
if
values
and
'ENGINE'
in
values
:
processors
.
append
(
self
.
instantiate_from_dict
(
values
))
return
backend
return
processors
def
override_default_tracker
():
...
...
eventtracking/django/tests/test_configuration.py
View file @
64a93514
...
...
@@ -137,6 +137,23 @@ class TestConfiguration(TestCase):
django
.
override_default_tracker
()
self
.
assertTrue
(
isinstance
(
tracker
.
get_tracker
(),
tracker
.
Tracker
))
@override_settings
(
TRACKING_PROCESSORS
=
[
{
'ENGINE'
:
'eventtracking.django.tests.test_configuration.NopProcessor'
}
])
def
test_single_processor
(
self
):
self
.
configure_tracker
()
self
.
assertEquals
(
len
(
self
.
tracker
.
processors
),
1
)
self
.
assertTrue
(
isinstance
(
self
.
tracker
.
processors
[
0
],
NopProcessor
))
@override_settings
(
TRACKING_PROCESSORS
=
[
{}
])
def
test_missing_processor_engine
(
self
):
self
.
configure_tracker
()
self
.
assertEquals
(
len
(
self
.
tracker
.
processors
),
0
)
class
TrivialFakeBackend
(
object
):
"""A trivial fake backend without any options"""
...
...
@@ -157,3 +174,10 @@ class FakeBackendWithOptions(TrivialFakeBackend):
def
__init__
(
self
,
**
kwargs
):
super
(
FakeBackendWithOptions
,
self
)
.
__init__
()
self
.
option
=
kwargs
.
get
(
'option'
,
None
)
class
NopProcessor
(
object
):
"""Changes every event"""
def
__call__
(
self
,
event
):
pass
eventtracking/tests/test_track.py
View file @
64a93514
...
...
@@ -184,3 +184,75 @@ class TestTrack(TestCase): # pylint: disable=missing-docstring
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
name
)
def
test_single_processor
(
self
):
self
.
tracker
.
processors
.
append
(
self
.
change_name
)
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
changed_name
)
def
change_name
(
self
,
event
):
"""Modify the event type of the event"""
event
[
'name'
]
=
sentinel
.
changed_name
return
event
def
test_non_callable_processor
(
self
):
self
.
tracker
.
processors
.
append
(
object
())
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
name
)
def
test_callable_class_processor
(
self
):
class
SampleProcessor
(
object
):
"""An event processing class"""
def
__call__
(
self
,
event
):
"""Modify the event type"""
event
[
'name'
]
=
sentinel
.
class_name
self
.
tracker
.
processors
.
append
(
SampleProcessor
())
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
class_name
)
def
test_processor_chain
(
self
):
def
ensure_modified_event
(
event
):
"""Assert the first processor added a field to the event"""
self
.
assertIn
(
sentinel
.
key
,
event
)
self
.
assertEquals
(
event
[
sentinel
.
key
],
sentinel
.
value
)
return
event
self
.
tracker
.
processors
.
extend
([
self
.
change_name
,
ensure_modified_event
])
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
changed_name
)
def
test_processor_failure
(
self
):
def
always_fail
(
event
):
# pylint: disable=unused-argument
"""Always raises an error"""
raise
ValueError
self
.
tracker
.
processors
.
extend
([
always_fail
,
self
.
change_name
])
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
changed_name
)
def
test_processor_returns_none
(
self
):
def
return_none
(
event
):
# pylint: disable=unused-argument
"""Don't return the event"""
pass
self
.
tracker
.
processors
.
append
(
return_none
)
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
name
)
def
test_processor_modifies_the_same_event_object
(
self
):
def
forget_return
(
event
):
"""Modify the event without returning it"""
event
[
'name'
]
=
sentinel
.
forgotten_return
def
ensure_name_changed
(
event
):
"""Assert the event type has been modified even though the event wasn't returned"""
self
.
assertEquals
(
event
[
'name'
],
sentinel
.
forgotten_return
)
self
.
tracker
.
processors
.
extend
([
forget_return
,
ensure_name_changed
])
self
.
tracker
.
emit
(
sentinel
.
name
)
self
.
assert_backend_called_with
(
sentinel
.
forgotten_return
)
eventtracking/tracker.py
View file @
64a93514
...
...
@@ -36,9 +36,14 @@ class Tracker(object):
Track application events. Holds references to a set of backends that will
be used to persist any events that are emitted.
"""
def
__init__
(
self
,
backends
=
None
,
context_locator
=
None
):
def
__init__
(
self
,
backends
=
None
,
context_locator
=
None
,
processors
=
None
):
self
.
backends
=
backends
or
{}
self
.
context_locator
=
context_locator
or
DefaultContextLocator
()
self
.
processors
=
processors
or
[]
for
backend
in
backends
.
itervalues
():
if
not
hasattr
(
backend
,
'send'
)
or
not
callable
(
backend
.
send
):
raise
ValueError
(
'Backend
%
s does not have a callable "send" method.'
%
backend
.
__class__
.
__name__
)
@property
def
located_context
(
self
):
...
...
@@ -61,16 +66,44 @@ class Tracker(object):
Note that all values provided must be serializable.
"""
full_
event
=
{
event
=
{
'name'
:
name
or
UNKNOWN_EVENT_TYPE
,
'timestamp'
:
datetime
.
now
(
UTC
),
'data'
:
data
or
{},
'context'
:
self
.
resolve_context
()
}
event
=
self
.
process_event
(
event
)
self
.
send_to_backends
(
event
)
def
process_event
(
self
,
event
):
"""
Executes all event processors on the event in order.
`event` is a nested dictionary that represents the event.
Returns the modified event.
"""
for
processor
in
self
.
processors
:
try
:
modified_event
=
processor
(
event
)
if
modified_event
is
not
None
:
event
=
modified_event
except
Exception
:
# pylint: disable=broad-except
LOG
.
exception
(
'Failed to execute processor: {0}'
.
format
(
processor
)
)
return
event
def
send_to_backends
(
self
,
event
):
"""Sends the event to all registered backends."""
for
name
,
backend
in
self
.
backends
.
iteritems
():
try
:
backend
.
send
(
full_
event
)
backend
.
send
(
event
)
except
Exception
:
# pylint: disable=broad-except
LOG
.
exception
(
'Unable to send event to backend: {0}'
.
format
(
name
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment