killbill-memoizeit

analytics: handle dups events gracefully It is possible

2/21/2012 7:36:02 PM

Details

diff --git a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
index d623338..1735062 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -36,14 +36,15 @@ public class AnalyticsListener
     }
 
     @Subscribe
-    public void handleSubscriptionTransitionChange(final SubscriptionTransition event) throws AccountApiException {
+    public void handleSubscriptionTransitionChange(final SubscriptionTransition event) throws AccountApiException
+    {
         switch (event.getTransitionType()) {
             case MIGRATE_ENTITLEMENT:
                 // TODO do nothing for now
-            break;
+                break;
             case CREATE:
                 bstRecorder.subscriptionCreated(event);
-            break;
+                break;
             case CANCEL:
                 bstRecorder.subscriptionCancelled(event);
                 break;
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransition.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransition.java
index 8da7ff0..6c3a393 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransition.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransition.java
@@ -18,6 +18,8 @@ package com.ning.billing.analytics;
 
 import org.joda.time.DateTime;
 
+import java.util.UUID;
+
 /**
  * Describe a state change between two BusinessSubscription
  * <p/>
@@ -25,6 +27,7 @@ import org.joda.time.DateTime;
  */
 public class BusinessSubscriptionTransition
 {
+    private final UUID id;
     private final String key;
     private final String accountKey;
     private final DateTime requestedTimestamp;
@@ -32,8 +35,11 @@ public class BusinessSubscriptionTransition
     private final BusinessSubscription previousSubscription;
     private final BusinessSubscription nextSubscription;
 
-    public BusinessSubscriptionTransition(final String key, final String accountKey, final DateTime requestedTimestamp, final BusinessSubscriptionEvent event, final BusinessSubscription previousSubscription, final BusinessSubscription nextsubscription)
+    public BusinessSubscriptionTransition(final UUID id, final String key, final String accountKey, final DateTime requestedTimestamp, final BusinessSubscriptionEvent event, final BusinessSubscription previousSubscription, final BusinessSubscription nextsubscription)
     {
+        if (id == null) {
+            throw new IllegalArgumentException("An event must have an id");
+        }
         if (key == null) {
             throw new IllegalArgumentException("An event must have an key");
         }
@@ -47,6 +53,7 @@ public class BusinessSubscriptionTransition
             throw new IllegalArgumentException("No event specified");
         }
 
+        this.id = id;
         this.key = key;
         this.accountKey = accountKey;
         this.requestedTimestamp = requestedTimestamp;
@@ -55,6 +62,11 @@ public class BusinessSubscriptionTransition
         this.nextSubscription = nextsubscription;
     }
 
+    public UUID getId()
+    {
+        return id;
+    }
+
     public BusinessSubscriptionEvent getEvent()
     {
         return event;
@@ -90,10 +102,11 @@ public class BusinessSubscriptionTransition
     {
         final StringBuilder sb = new StringBuilder();
         sb.append("BusinessSubscriptionTransition");
-        sb.append("{event=").append(event);
+        sb.append("{accountKey='").append(accountKey).append('\'');
+        sb.append(", id=").append(id);
         sb.append(", key='").append(key).append('\'');
-        sb.append(", accountKey='").append(accountKey).append('\'');
         sb.append(", requestedTimestamp=").append(requestedTimestamp);
+        sb.append(", event=").append(event);
         sb.append(", previousSubscription=").append(previousSubscription);
         sb.append(", nextSubscription=").append(nextSubscription);
         sb.append('}');
@@ -112,13 +125,16 @@ public class BusinessSubscriptionTransition
 
         final BusinessSubscriptionTransition that = (BusinessSubscriptionTransition) o;
 
+        if (accountKey != null ? !accountKey.equals(that.accountKey) : that.accountKey != null) {
+            return false;
+        }
         if (event != null ? !event.equals(that.event) : that.event != null) {
             return false;
         }
-        if (key != null ? !key.equals(that.key) : that.key != null) {
+        if (id != null ? !id.equals(that.id) : that.id != null) {
             return false;
         }
-        if (accountKey != null ? !accountKey.equals(that.accountKey) : that.accountKey != null) {
+        if (key != null ? !key.equals(that.key) : that.key != null) {
             return false;
         }
         if (nextSubscription != null ? !nextSubscription.equals(that.nextSubscription) : that.nextSubscription != null) {
@@ -137,7 +153,8 @@ public class BusinessSubscriptionTransition
     @Override
     public int hashCode()
     {
-        int result = key != null ? key.hashCode() : 0;
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (key != null ? key.hashCode() : 0);
         result = 31 * result + (accountKey != null ? accountKey.hashCode() : 0);
         result = 31 * result + (requestedTimestamp != null ? requestedTimestamp.hashCode() : 0);
         result = 31 * result + (event != null ? event.hashCode() : 0);
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
index 61dc207..438850f 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
@@ -30,6 +30,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.List;
+import java.util.UUID;
 
 public class BusinessSubscriptionTransitionRecorder
 {
@@ -131,13 +132,14 @@ public class BusinessSubscriptionTransitionRecorder
             nextSubscription = new BusinessSubscription(transition.getNextPriceList(), transition.getNextPlan(), transition.getNextPhase(), currency, transition.getEffectiveTransitionTime(), transition.getNextState(), transition.getSubscriptionId(), transition.getBundleId());
         }
 
-        record(transitionKey, accountKey, transition.getRequestedTransitionTime(), event, prevSubscription, nextSubscription);
+        record(transition.getId(), transitionKey, accountKey, transition.getRequestedTransitionTime(), event, prevSubscription, nextSubscription);
     }
 
     // Public for internal reasons
-    public void record(final String key, final String accountKey, final DateTime requestedDateTime, final BusinessSubscriptionEvent event, final BusinessSubscription prevSubscription, final BusinessSubscription nextSubscription)
+    public void record(final UUID id, final String key, final String accountKey, final DateTime requestedDateTime, final BusinessSubscriptionEvent event, final BusinessSubscription prevSubscription, final BusinessSubscription nextSubscription)
     {
         final BusinessSubscriptionTransition transition = new BusinessSubscriptionTransition(
+            id,
             key,
             accountKey,
             requestedDateTime,
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionBinder.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionBinder.java
index 374f296..b769e5a 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionBinder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionBinder.java
@@ -43,6 +43,7 @@ public @interface BusinessSubscriptionTransitionBinder
             {
                 public void bind(final SQLStatement q, final BusinessSubscriptionTransitionBinder bind, final BusinessSubscriptionTransition arg)
                 {
+                    q.bind("event_id", arg.getId().toString());
                     q.bind("event_key", arg.getKey());
                     q.bind("account_key", arg.getAccountKey());
                     q.bind("requested_timestamp", arg.getRequestedTimestamp().getMillis());
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionMapper.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionMapper.java
index a31d104..ed41ab5 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionMapper.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionMapper.java
@@ -38,20 +38,20 @@ public class BusinessSubscriptionTransitionMapper implements ResultSetMapper<Bus
     public BusinessSubscriptionTransition map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException
     {
         BusinessSubscription prev = new BusinessSubscription(
-            r.getString(5), // productName
-            r.getString(6), // productType
-            r.getString(7) == null ? null : ProductCategory.valueOf(r.getString(7)), // productCategory
-            r.getString(8), // slug
-            r.getString(9),  // phase
-            r.getString(10),  // billing period
-            BigDecimal.valueOf(r.getDouble(11)), // price
-            r.getString(12), // priceList
-            BigDecimal.valueOf(r.getDouble(13)), // mrr
-            r.getString(14), // currency
-            r.getLong(15) == 0 ? null : new DateTime(r.getLong(15), DateTimeZone.UTC), // startDate
-            r.getString(16) == null ? null : SubscriptionState.valueOf(r.getString(16)), // state
-            r.getString(17) == null ? null : UUID.fromString(r.getString(17)), // subscriptionId
-            r.getString(18) == null ? null : UUID.fromString(r.getString(18)) //bundleId
+            r.getString(6), // productName
+            r.getString(7), // productType
+            r.getString(8) == null ? null : ProductCategory.valueOf(r.getString(8)), // productCategory
+            r.getString(9), // slug
+            r.getString(10),  // phase
+            r.getString(11),  // billing period
+            BigDecimal.valueOf(r.getDouble(12)), // price
+            r.getString(13), // priceList
+            BigDecimal.valueOf(r.getDouble(14)), // mrr
+            r.getString(15), // currency
+            r.getLong(16) == 0 ? null : new DateTime(r.getLong(16), DateTimeZone.UTC), // startDate
+            r.getString(17) == null ? null : SubscriptionState.valueOf(r.getString(17)), // state
+            r.getString(18) == null ? null : UUID.fromString(r.getString(18)), // subscriptionId
+            r.getString(19) == null ? null : UUID.fromString(r.getString(19)) //bundleId
         );
 
         // Avoid creating a dummy subscriptions with all null fields
@@ -60,20 +60,20 @@ public class BusinessSubscriptionTransitionMapper implements ResultSetMapper<Bus
         }
 
         BusinessSubscription next = new BusinessSubscription(
-            r.getString(19), // productName
-            r.getString(20), // productType
-            r.getString(21) == null ? null : ProductCategory.valueOf(r.getString(21)), // productCategory
-            r.getString(22), // slug8
-            r.getString(23),  // phase
-            r.getString(24),  // billing period
-            BigDecimal.valueOf(r.getDouble(25)), // price
-            r.getString(26), // priceList
-            BigDecimal.valueOf(r.getDouble(27)), // mrr
-            r.getString(28), // currency
-            r.getLong(29) == 0 ? null : new DateTime(r.getLong(29), DateTimeZone.UTC), // startDate
-            r.getString(30) == null ? null : SubscriptionState.valueOf(r.getString(30)), // state
-            r.getString(31) == null ? null : UUID.fromString(r.getString(31)), // subscriptionId
-            r.getString(32) == null ? null : UUID.fromString(r.getString(32)) //bundleId
+            r.getString(20), // productName
+            r.getString(21), // productType
+            r.getString(22) == null ? null : ProductCategory.valueOf(r.getString(22)), // productCategory
+            r.getString(23), // slug8
+            r.getString(24),  // phase
+            r.getString(25),  // billing period
+            BigDecimal.valueOf(r.getDouble(26)), // price
+            r.getString(27), // priceList
+            BigDecimal.valueOf(r.getDouble(28)), // mrr
+            r.getString(29), // currency
+            r.getLong(30) == 0 ? null : new DateTime(r.getLong(30), DateTimeZone.UTC), // startDate
+            r.getString(31) == null ? null : SubscriptionState.valueOf(r.getString(31)), // state
+            r.getString(32) == null ? null : UUID.fromString(r.getString(32)), // subscriptionId
+            r.getString(33) == null ? null : UUID.fromString(r.getString(33)) //bundleId
         );
 
         // Avoid creating a dummy subscriptions with all null fields
@@ -81,12 +81,13 @@ public class BusinessSubscriptionTransitionMapper implements ResultSetMapper<Bus
             next = null;
         }
 
-        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.valueOf(r.getString(4));
+        final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.valueOf(r.getString(5));
 
         return new BusinessSubscriptionTransition(
-            r.getString(1),
+            UUID.fromString(r.getString(1)),
             r.getString(2),
-            new DateTime(r.getLong(3), DateTimeZone.UTC),
+            r.getString(3),
+            new DateTime(r.getLong(4), DateTimeZone.UTC),
             event,
             prev,
             next
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionDao.sql.stg
index 1654b5b..de6076e 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionDao.sql.stg
@@ -2,7 +2,8 @@ group BusinessSubscriptionTransition;
 
 getTransitions(event_key) ::= <<
   select
-    event_key
+    event_id
+  , event_key
   , account_key
   , requested_timestamp
   , event
@@ -41,8 +42,9 @@ getTransitions(event_key) ::= <<
 >>
 
 createTransition() ::= <<
-  insert into bst(
-    event_key
+  insert ignore into bst(
+    event_id
+  , event_key
   , account_key
   , requested_timestamp
   , event
@@ -75,7 +77,8 @@ createTransition() ::= <<
   , next_subscription_id
   , next_bundle_id
   ) values (
-    :event_key
+    :event_id
+  , :event_key
   , :account_key
   , :requested_timestamp
   , :event
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
index 49e48f0..6489b12 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
+++ b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
@@ -1,6 +1,7 @@
 drop table if exists bst;
 create table bst (
-  event_key varchar(50) not null
+  event_id char(36) not null
+, event_key varchar(50) not null
 , account_key varchar(50) not null
 , requested_timestamp bigint not null
 , event varchar(50) not null
@@ -32,6 +33,7 @@ create table bst (
 , next_state varchar(32) default null
 , next_subscription_id varchar(100) default null
 , next_bundle_id varchar(100) default null
+, primary key(event_id)
 ) engine=innodb;
 create index bst_key_index on bst (event_key, requested_timestamp asc);
 
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index 6054529..d869e88 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -72,6 +72,7 @@ import static org.testng.Assert.fail;
 @Guice(modules = AnalyticsTestModule.class)
 public class TestAnalyticsService
 {
+    private static final UUID ID = UUID.randomUUID();
     private static final String KEY = "12345";
     private static final String ACCOUNT_KEY = "pierre-12345";
     private static final DefaultTagDefinition TAG_ONE = new DefaultTagDefinition("batch20", "something", "pierre", new DateTime(DateTimeZone.UTC));
@@ -165,7 +166,7 @@ public class TestAnalyticsService
         final String priceList = "something";
 
         transition = new SubscriptionTransitionData(
-            UUID.randomUUID(),
+            ID,
             subscriptionId,
             bundle.getId(),
             EntitlementEvent.EventType.API_USER,
@@ -182,6 +183,7 @@ public class TestAnalyticsService
             priceList
         );
         expectedTransition = new BusinessSubscriptionTransition(
+            ID,
             KEY,
             ACCOUNT_KEY,
             requestedTransitionTime,
diff --git a/analytics/src/test/java/com/ning/billing/analytics/dao/TestAnalyticsDao.java b/analytics/src/test/java/com/ning/billing/analytics/dao/TestAnalyticsDao.java
index c17cdd4..4c7cc2c 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/dao/TestAnalyticsDao.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/dao/TestAnalyticsDao.java
@@ -52,6 +52,7 @@ import java.util.UUID;
 
 public class TestAnalyticsDao
 {
+    private static final UUID EVENT_ID = UUID.randomUUID();
     private static final String EVENT_KEY = "12345";
     private static final String ACCOUNT_KEY = "pierre-143343-vcc";
 
@@ -84,7 +85,7 @@ public class TestAnalyticsDao
         final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCancelled(plan);
         final DateTime requestedTimestamp = new DateTime(DateTimeZone.UTC);
 
-        transition = new BusinessSubscriptionTransition(EVENT_KEY, ACCOUNT_KEY, requestedTimestamp, event, prevSubscription, nextSubscription);
+        transition = new BusinessSubscriptionTransition(EVENT_ID, EVENT_KEY, ACCOUNT_KEY, requestedTimestamp, event, prevSubscription, nextSubscription);
 
         final IDBI dbi = helper.getDBI();
         businessSubscriptionTransitionDao = dbi.onDemand(BusinessSubscriptionTransitionDao.class);
@@ -133,6 +134,7 @@ public class TestAnalyticsDao
     public void testTransitionsWithNullPrevSubscription()
     {
         final BusinessSubscriptionTransition transitionWithNullPrev = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
@@ -151,6 +153,7 @@ public class TestAnalyticsDao
     public void testTransitionsWithNullNextSubscription()
     {
         final BusinessSubscriptionTransition transitionWithNullNext = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
@@ -170,6 +173,7 @@ public class TestAnalyticsDao
     {
         final BusinessSubscription subscriptionWithNullFields = new BusinessSubscription(null, plan, phase, Currency.USD, null, null, null, null);
         final BusinessSubscriptionTransition transitionWithNullFields = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
@@ -189,6 +193,7 @@ public class TestAnalyticsDao
     {
         final BusinessSubscription subscriptionWithNullPlanAndPhase = new BusinessSubscription(null, null, null, Currency.USD, null, null, null, null);
         final BusinessSubscriptionTransition transitionWithNullPlanAndPhase = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
@@ -213,6 +218,7 @@ public class TestAnalyticsDao
     {
         final BusinessSubscription subscriptionWithNullPlan = new BusinessSubscription(null, null, phase, Currency.USD, null, null, null, null);
         final BusinessSubscriptionTransition transitionWithNullPlan = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
@@ -233,6 +239,7 @@ public class TestAnalyticsDao
     {
         final BusinessSubscription subscriptionWithNullPhase = new BusinessSubscription(null, plan, null, Currency.USD, null, null, null, null);
         final BusinessSubscriptionTransition transitionWithNullPhase = new BusinessSubscriptionTransition(
+            transition.getId(),
             transition.getKey(),
             transition.getAccountKey(),
             transition.getRequestedTimestamp(),
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
index 9f9e3c8..a9a8293 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
@@ -67,7 +67,7 @@ public class TestAnalyticsListener
         final DateTime effectiveTransitionTime = new DateTime(DateTimeZone.UTC);
         final DateTime requestedTransitionTime = new DateTime(DateTimeZone.UTC);
         final SubscriptionTransitionData firstTransition = createFirstSubscriptionTransition(requestedTransitionTime, effectiveTransitionTime);
-        final BusinessSubscriptionTransition firstBST = createExpectedFirstBST(requestedTransitionTime, effectiveTransitionTime);
+        final BusinessSubscriptionTransition firstBST = createExpectedFirstBST(firstTransition.getId(), requestedTransitionTime, effectiveTransitionTime);
         listener.handleSubscriptionTransitionChange(firstTransition);
         Assert.assertEquals(dao.getTransitions(KEY).size(), 1);
         Assert.assertEquals(dao.getTransitions(KEY).get(0), firstBST);
@@ -76,7 +76,7 @@ public class TestAnalyticsListener
         final DateTime effectivePauseTransitionTime = new DateTime(DateTimeZone.UTC);
         final DateTime requestedPauseTransitionTime = new DateTime(DateTimeZone.UTC);
         final SubscriptionTransitionData pausedSubscriptionTransition = createPauseSubscriptionTransition(effectivePauseTransitionTime, requestedPauseTransitionTime, firstTransition.getNextState());
-        final BusinessSubscriptionTransition pausedBST = createExpectedPausedBST(requestedPauseTransitionTime, effectivePauseTransitionTime, firstBST.getNextSubscription());
+        final BusinessSubscriptionTransition pausedBST = createExpectedPausedBST(pausedSubscriptionTransition.getId(), requestedPauseTransitionTime, effectivePauseTransitionTime, firstBST.getNextSubscription());
         listener.handleSubscriptionTransitionChange(pausedSubscriptionTransition);
         Assert.assertEquals(dao.getTransitions(KEY).size(), 2);
         Assert.assertEquals(dao.getTransitions(KEY).get(1), pausedBST);
@@ -85,7 +85,7 @@ public class TestAnalyticsListener
         final DateTime effectiveResumeTransitionTime = new DateTime(DateTimeZone.UTC);
         final DateTime requestedResumeTransitionTime = new DateTime(DateTimeZone.UTC);
         final SubscriptionTransitionData resumedSubscriptionTransition = createResumeSubscriptionTransition(requestedResumeTransitionTime, effectiveResumeTransitionTime, pausedSubscriptionTransition.getNextState());
-        final BusinessSubscriptionTransition resumedBST = createExpectedResumedBST(requestedResumeTransitionTime, effectiveResumeTransitionTime, pausedBST.getNextSubscription());
+        final BusinessSubscriptionTransition resumedBST = createExpectedResumedBST(resumedSubscriptionTransition.getId(), requestedResumeTransitionTime, effectiveResumeTransitionTime, pausedBST.getNextSubscription());
         listener.handleSubscriptionTransitionChange(resumedSubscriptionTransition);
         Assert.assertEquals(dao.getTransitions(KEY).size(), 3);
         Assert.assertEquals(dao.getTransitions(KEY).get(2), resumedBST);
@@ -93,41 +93,42 @@ public class TestAnalyticsListener
         // Cancel it
         final DateTime effectiveCancelTransitionTime = new DateTime(DateTimeZone.UTC);
         final DateTime requestedCancelTransitionTime = new DateTime(DateTimeZone.UTC);
-        listener.handleSubscriptionTransitionChange(createCancelSubscriptionTransition(requestedCancelTransitionTime, effectiveCancelTransitionTime, resumedSubscriptionTransition.getNextState()));
-        final BusinessSubscriptionTransition cancelledBST = createExpectedCancelledBST(requestedCancelTransitionTime, effectiveCancelTransitionTime, resumedBST.getNextSubscription());
+        final SubscriptionTransitionData cancelledSubscriptionTransition = createCancelSubscriptionTransition(requestedCancelTransitionTime, effectiveCancelTransitionTime, resumedSubscriptionTransition.getNextState());
+        final BusinessSubscriptionTransition cancelledBST = createExpectedCancelledBST(cancelledSubscriptionTransition.getId(), requestedCancelTransitionTime, effectiveCancelTransitionTime, resumedBST.getNextSubscription());
+        listener.handleSubscriptionTransitionChange(cancelledSubscriptionTransition);
         Assert.assertEquals(dao.getTransitions(KEY).size(), 4);
         Assert.assertEquals(dao.getTransitions(KEY).get(3), cancelledBST);
     }
 
-    private BusinessSubscriptionTransition createExpectedFirstBST(final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime)
+    private BusinessSubscriptionTransition createExpectedFirstBST(final UUID id, final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime)
     {
         final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCreated(plan);
         final Subscription.SubscriptionState subscriptionState = Subscription.SubscriptionState.ACTIVE;
-        return createExpectedBST(event, requestedTransitionTime, effectiveTransitionTime, null, subscriptionState);
+        return createExpectedBST(id, event, requestedTransitionTime, effectiveTransitionTime, null, subscriptionState);
     }
 
-    private BusinessSubscriptionTransition createExpectedPausedBST(final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
+    private BusinessSubscriptionTransition createExpectedPausedBST(final UUID id, final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
     {
         final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionPaused(plan);
         final Subscription.SubscriptionState subscriptionState = Subscription.SubscriptionState.PAUSED;
-        return createExpectedBST(event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, subscriptionState);
+        return createExpectedBST(id, event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, subscriptionState);
     }
 
-    private BusinessSubscriptionTransition createExpectedResumedBST(final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
+    private BusinessSubscriptionTransition createExpectedResumedBST(final UUID id, final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
     {
         final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionResumed(plan);
         final Subscription.SubscriptionState nextState = Subscription.SubscriptionState.ACTIVE;
-        return createExpectedBST(event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, nextState);
+        return createExpectedBST(id, event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, nextState);
     }
 
-    private BusinessSubscriptionTransition createExpectedCancelledBST(final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
+    private BusinessSubscriptionTransition createExpectedCancelledBST(final UUID id, final DateTime requestedTransitionTime, final DateTime effectiveTransitionTime, final BusinessSubscription lastSubscription)
     {
         final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCancelled(plan);
-        final Subscription.SubscriptionState nextState = Subscription.SubscriptionState.CANCELLED;
-        return createExpectedBST(event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, null);
+        return createExpectedBST(id, event, requestedTransitionTime, effectiveTransitionTime, lastSubscription, null);
     }
 
     private BusinessSubscriptionTransition createExpectedBST(
+        final UUID eventId,
         final BusinessSubscriptionEvent eventType,
         final DateTime requestedTransitionTime,
         final DateTime effectiveTransitionTime,
@@ -136,6 +137,7 @@ public class TestAnalyticsListener
     )
     {
         return new BusinessSubscriptionTransition(
+            eventId,
             KEY,
             ACCOUNT_KEY,
             requestedTransitionTime,
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransition.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransition.java
index a8a954c..3b33bef 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransition.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransition.java
@@ -28,6 +28,8 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.util.UUID;
+
 import static com.ning.billing.catalog.api.Currency.USD;
 
 public class TestBusinessSubscriptionTransition
@@ -36,6 +38,7 @@ public class TestBusinessSubscriptionTransition
     private BusinessSubscription nextSubscription;
     private BusinessSubscriptionEvent event;
     private DateTime requestedTimestamp;
+    private UUID id;
     private String key;
     private String accountKey;
     private BusinessSubscriptionTransition transition;
@@ -53,9 +56,10 @@ public class TestBusinessSubscriptionTransition
         nextSubscription = new BusinessSubscription(nextISubscription, USD);
         event = BusinessSubscriptionEvent.subscriptionCancelled(prevISubscription.getCurrentPlan());
         requestedTimestamp = new DateTime(DateTimeZone.UTC);
+        id = UUID.randomUUID();
         key = "1234";
         accountKey = "pierre-1234";
-        transition = new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
+        transition = new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
     }
 
     @Test(groups = "fast")
@@ -76,22 +80,22 @@ public class TestBusinessSubscriptionTransition
 
         BusinessSubscriptionTransition otherTransition;
 
-        otherTransition = new BusinessSubscriptionTransition(key, accountKey, new DateTime(), event, prevSubscription, nextSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, key, accountKey, new DateTime(), event, prevSubscription, nextSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
 
-        otherTransition = new BusinessSubscriptionTransition("12345", accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, "12345", accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
 
-        otherTransition = new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, BusinessSubscriptionEvent.subscriptionPaused(null), prevSubscription, nextSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, BusinessSubscriptionEvent.subscriptionPaused(null), prevSubscription, nextSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
 
-        otherTransition = new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, event, prevSubscription, prevSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, event, prevSubscription, prevSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
 
-        otherTransition = new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, event, nextSubscription, nextSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, event, nextSubscription, nextSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
 
-        otherTransition = new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, event, nextSubscription, prevSubscription);
+        otherTransition = new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, event, nextSubscription, prevSubscription);
         Assert.assertTrue(!transition.equals(otherTransition));
     }
 
@@ -99,7 +103,15 @@ public class TestBusinessSubscriptionTransition
     public void testRejectInvalidTransitions() throws Exception
     {
         try {
-            new BusinessSubscriptionTransition(null, accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
+            new BusinessSubscriptionTransition(null, key, accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
+            Assert.fail();
+        }
+        catch (IllegalArgumentException e) {
+            Assert.assertTrue(true);
+        }
+
+        try {
+            new BusinessSubscriptionTransition(id, null, accountKey, requestedTimestamp, event, prevSubscription, nextSubscription);
             Assert.fail();
         }
         catch (IllegalArgumentException e) {
@@ -107,7 +119,7 @@ public class TestBusinessSubscriptionTransition
         }
 
         try {
-            new BusinessSubscriptionTransition(key, accountKey, null, event, prevSubscription, nextSubscription);
+            new BusinessSubscriptionTransition(id, key, accountKey, null, event, prevSubscription, nextSubscription);
             Assert.fail();
         }
         catch (IllegalArgumentException e) {
@@ -115,7 +127,7 @@ public class TestBusinessSubscriptionTransition
         }
 
         try {
-            new BusinessSubscriptionTransition(key, accountKey, requestedTimestamp, null, prevSubscription, nextSubscription);
+            new BusinessSubscriptionTransition(id, key, accountKey, requestedTimestamp, null, prevSubscription, nextSubscription);
             Assert.fail();
         }
         catch (IllegalArgumentException e) {