001/* 002 * (C) Copyright 2014-2017 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * <a href="mailto:[email protected]">Tiry</a> 018 * Yannis JULIENNE 019 */ 020 021package org.nuxeo.segment.io; 022 023import java.io.Serializable; 024import java.lang.reflect.Field; 025import java.util.ArrayList; 026import java.util.HashMap; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Set; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.joda.time.DateTime; 036import org.nuxeo.ecm.core.api.NuxeoPrincipal; 037import org.nuxeo.runtime.api.Framework; 038import org.nuxeo.runtime.model.ComponentContext; 039import org.nuxeo.runtime.model.ComponentInstance; 040import org.nuxeo.runtime.model.DefaultComponent; 041import org.osgi.framework.Bundle; 042 043import com.github.segmentio.Analytics; 044import com.github.segmentio.AnalyticsClient; 045import com.github.segmentio.flush.Flusher; 046import com.github.segmentio.models.Group; 047import com.github.segmentio.models.Options; 048import com.github.segmentio.models.Props; 049import com.github.segmentio.models.Traits; 050 051/** 052 * @author <a href="mailto:[email protected]">Tiry</a> 053 */ 054public class SegmentIOComponent extends DefaultComponent implements SegmentIO { 055 056 protected static Log log = LogFactory.getLog(SegmentIOComponent.class); 057 058 protected static final String DEFAULT_DEBUG_KEY = "FakeKey_ChangeMe"; 059 060 public final static String WRITE_KEY = "segment.io.write.key"; 061 062 public final static String CONFIG_EP = "config"; 063 064 public final static String MAPPER_EP = "mapper"; 065 066 public final static String INTEGRATIONS_EP = "integrations"; 067 068 public final static String FILTERS_EP = "filters"; 069 070 protected boolean debugMode = false; 071 072 protected Map<String, SegmentIOMapper> mappers; 073 074 protected Map<String, List<SegmentIOMapper>> event2Mappers = new HashMap<>(); 075 076 protected List<Map<String, Object>> testData = new LinkedList<>(); 077 078 protected SegmentIOConfig config; 079 080 protected SegmentIOIntegrations integrationsConfig; 081 082 protected SegmentIOUserFilter userFilters; 083 084 protected Bundle bundle; 085 086 protected Flusher flusher; 087 088 public Bundle getBundle() { 089 return bundle; 090 } 091 092 @Override 093 public void activate(ComponentContext context) { 094 bundle = context.getRuntimeContext().getBundle(); 095 mappers = new HashMap<>(); 096 } 097 098 @Override 099 public void deactivate(ComponentContext context) { 100 flush(); 101 bundle = null; 102 } 103 104 @Override 105 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 106 if (CONFIG_EP.equalsIgnoreCase(extensionPoint)) { 107 config = (SegmentIOConfig) contribution; 108 } else if (MAPPER_EP.equalsIgnoreCase(extensionPoint)) { 109 SegmentIOMapper mapper = (SegmentIOMapper) contribution; 110 mappers.put(mapper.name, mapper); 111 } else if (INTEGRATIONS_EP.equalsIgnoreCase(extensionPoint)) { 112 integrationsConfig = (SegmentIOIntegrations) contribution; 113 } else if (FILTERS_EP.equalsIgnoreCase(extensionPoint)) { 114 userFilters = (SegmentIOUserFilter) contribution; 115 } 116 } 117 118 @Override 119 public void applicationStarted(ComponentContext context) { 120 String key = getWriteKey(); 121 if (DEFAULT_DEBUG_KEY.equals(key)) { 122 log.info("Run Segment.io in debug mode : nothing will be sent to the server"); 123 debugMode = true; 124 } else { 125 Analytics.initialize(key); 126 } 127 computeEvent2Mappers(); 128 } 129 130 protected void computeEvent2Mappers() { 131 event2Mappers = new HashMap<String, List<SegmentIOMapper>>(); 132 for (SegmentIOMapper mapper : mappers.values()) { 133 for (String event : mapper.events) { 134 List<SegmentIOMapper> m4event = event2Mappers.get(event); 135 if (m4event == null) { 136 event2Mappers.put(event, new ArrayList<SegmentIOMapper>()); 137 m4event = event2Mappers.get(event); 138 } 139 if (!m4event.contains(mapper)) { 140 m4event.add(mapper); 141 } 142 } 143 } 144 } 145 146 @Override 147 public String getWriteKey() { 148 if (config != null) { 149 if (config.writeKey != null) { 150 return config.writeKey; 151 } 152 } 153 return Framework.getProperty(WRITE_KEY, DEFAULT_DEBUG_KEY); 154 } 155 156 @Override 157 public Map<String, String> getGlobalParameters() { 158 if (config != null) { 159 if (config.parameters != null) { 160 return config.parameters; 161 } 162 } 163 return new HashMap<>(); 164 } 165 166 protected Flusher getFlusher() { 167 if (flusher == null) { 168 try { 169 AnalyticsClient client = Analytics.getDefaultClient(); 170 Field field = client.getClass().getDeclaredField("flusher"); 171 field.setAccessible(true); 172 flusher = (Flusher) field.get(client); 173 } catch (ReflectiveOperationException e) { 174 log.error("Unable to access SegmentIO Flusher via reflection", e); 175 } 176 } 177 return flusher; 178 } 179 180 @Override 181 public void identify(NuxeoPrincipal principal) { 182 identify(principal, null); 183 } 184 185 @Override 186 public Map<String, Boolean> getIntegrations() { 187 if (integrationsConfig != null && integrationsConfig.integrations != null) { 188 return integrationsConfig.integrations; 189 } 190 return new HashMap<>(); 191 } 192 193 /** 194 * Build common options for identify and track calls. These options contains the configured integrations values and 195 * the current timestamp. 196 * 197 * @return the builded {@link Options} object 198 */ 199 protected Options buildOptions() { 200 Options options = new Options(); 201 for (Entry<String, Boolean> integration : getIntegrations().entrySet()) { 202 options.setIntegration(integration.getKey(), integration.getValue()); 203 } 204 return options.setTimestamp(new DateTime()); 205 } 206 207 @Override 208 public void identify(NuxeoPrincipal principal, Map<String, Serializable> metadata) { 209 210 SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal, metadata); 211 212 if (!mustTrackprincipal(wrapper.getUserId())) { 213 if (log.isDebugEnabled()) { 214 log.debug("Skip user " + principal.getName()); 215 } 216 return; 217 } 218 219 if (debugMode) { 220 if (log.isInfoEnabled()) { 221 log.info("send identify for " + wrapper.getUserId() + " with meta : " + metadata.toString()); 222 } 223 } else { 224 if (log.isDebugEnabled()) { 225 log.debug("send identify with " + metadata.toString()); 226 } 227 Traits traits = new Traits(); 228 traits.putAll(wrapper.getMetadata()); 229 Options options = buildOptions(); 230 if (Framework.isTestModeSet()) { 231 pushForTest("identify", wrapper.getUserId(), traits, options); 232 } else { 233 Analytics.getDefaultClient().identify(wrapper.getUserId(), traits, options); 234 } 235 236 Map<String, Serializable> groupMeta = wrapper.getGroupMetadata(); 237 if (groupMeta.size() > 0 && groupMeta.containsKey("id")) { 238 Traits gtraits = new Traits(); 239 gtraits.putAll(groupMeta); 240 group((String) groupMeta.get("id"), wrapper.getUserId(), gtraits, options); 241 } else { 242 // automatic grouping 243 if (principal.getCompany() != null) { 244 group(principal.getCompany(), wrapper.getUserId(), null, options); 245 } else if (wrapper.getMetadata().get("company") != null) { 246 group((String) wrapper.getMetadata().get("company"), wrapper.getUserId(), null, options); 247 } 248 } 249 } 250 } 251 252 protected void group(String groupId, String userId, Traits traits, Options options) { 253 if (groupId == null || groupId.isEmpty()) { 254 return; 255 } 256 257 if (Framework.isTestModeSet()) { 258 pushForTest("group", userId, traits, options); 259 } else { 260 Flusher flusher = getFlusher(); 261 if (flusher != null) { 262 Group grp = new Group(userId, groupId, traits, options); 263 flusher.enqueue(grp); 264 } else { 265 log.warn("Can not use Group API"); 266 } 267 } 268 } 269 270 protected Map<String, Object> pushForTest(String action, String userId, Map<String, Object> metadata, 271 Options options) { 272 Map<String, Object> data = new HashMap<>(); 273 data.put("action", action); 274 data.put(SegmentIODataWrapper.PRINCIPAL_KEY, userId); 275 if (metadata != null) { 276 data.putAll(metadata); 277 } 278 if (options != null) { 279 data.put("options", options); 280 } 281 testData.add(data); 282 return data; 283 } 284 285 protected void pushForTest(String action, String userId, String eventName, Map<String, Object> metadata, 286 Options options) { 287 Map<String, Object> data = pushForTest(action, userId, metadata, options); 288 data.put("eventName", eventName); 289 } 290 291 public List<Map<String, Object>> getTestData() { 292 return testData; 293 } 294 295 public boolean mustTrackprincipal(String principalName) { 296 SegmentIOUserFilter filter = getUserFilters(); 297 if (filter == null) { 298 return true; 299 } 300 return filter.canTrack(principalName); 301 } 302 303 @Override 304 public void track(NuxeoPrincipal principal, String eventName, Map<String, Serializable> metadata) { 305 SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal, metadata); 306 Props properties = generateProperties(wrapper, ACTIONS.track.name(), eventName); 307 if (properties != null) { 308 Analytics.getDefaultClient().track(wrapper.getUserId(), eventName, properties, buildOptions()); 309 } 310 } 311 312 /** 313 * Generates a Analytics Props object. If user is ignored, or the execution in a test context the return is null and 314 * has been handled as app log. 315 * 316 * @return a filled Props object if the object has to be send for real. 317 */ 318 protected Props generateProperties(SegmentIODataWrapper wrapper, String action, String name) { 319 if (!mustTrackprincipal(wrapper.getUserId())) { 320 if (log.isDebugEnabled()) { 321 log.debug("Skip user " + wrapper.getUserId()); 322 } 323 return null; 324 } 325 326 if (debugMode) { 327 if (log.isInfoEnabled()) { 328 log.info(String.format("Send %s for %s user : %s with meta : %s", action, name, wrapper.getUserId(), 329 wrapper.getMetadata().toString())); 330 } 331 } else { 332 if (log.isDebugEnabled()) { 333 log.debug(String.format("Send %s with %s", action, wrapper.getMetadata().toString())); 334 } 335 Props eventProperties = new Props(); 336 eventProperties.putAll(wrapper.getMetadata()); 337 if (Framework.isTestModeSet()) { 338 pushForTest(action, wrapper.getUserId(), name, eventProperties, buildOptions()); 339 } else { 340 return eventProperties; 341 } 342 } 343 344 return null; 345 } 346 347 @Override 348 public void screen(NuxeoPrincipal principal, String screen, Map<String, Serializable> metadata) { 349 SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal, metadata); 350 Props properties = generateProperties(wrapper, ACTIONS.screen.name(), screen); 351 if (properties != null) { 352 Analytics.getDefaultClient().screen(wrapper.getUserId(), screen, properties, buildOptions()); 353 } 354 } 355 356 @Override 357 public void page(NuxeoPrincipal principal, String name, Map<String, Serializable> metadata) { 358 this.page(principal, name, null, metadata); 359 } 360 361 @Override 362 public void page(NuxeoPrincipal principal, String name, String category, Map<String, Serializable> metadata) { 363 SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal, metadata); 364 Props properties = generateProperties(wrapper, ACTIONS.page.name(), name); 365 if (properties != null) { 366 Analytics.getDefaultClient().page(wrapper.getUserId(), name, category, properties, buildOptions()); 367 } 368 } 369 370 @Override 371 public void flush() { 372 if (!debugMode) { 373 // only flush if Analytics was actually initialized 374 Analytics.flush(); 375 } 376 } 377 378 @Override 379 public Map<String, List<SegmentIOMapper>> getMappers(List<String> events) { 380 Map<String, List<SegmentIOMapper>> targetMappers = new HashMap<String, List<SegmentIOMapper>>(); 381 for (String event : events) { 382 if (event2Mappers.containsKey(event)) { 383 targetMappers.put(event, event2Mappers.get(event)); 384 } 385 } 386 return targetMappers; 387 } 388 389 @Override 390 public Set<String> getMappedEvents() { 391 return event2Mappers.keySet(); 392 } 393 394 @Override 395 public Map<String, List<SegmentIOMapper>> getAllMappers() { 396 return event2Mappers; 397 } 398 399 @Override 400 public SegmentIOUserFilter getUserFilters() { 401 return userFilters; 402 } 403 404 @Override 405 public boolean isDebugMode() { 406 return debugMode; 407 } 408}