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:tdelprat@nuxeo.com">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:tdelprat@nuxeo.com">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 protected 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) { 305 track(principal, null); 306 } 307 308 @Override 309 public void track(NuxeoPrincipal principal, String eventName, Map<String, Serializable> metadata) { 310 311 SegmentIODataWrapper wrapper = new SegmentIODataWrapper(principal, metadata); 312 313 if (!mustTrackprincipal(wrapper.getUserId())) { 314 if (log.isDebugEnabled()) { 315 log.debug("Skip user " + principal.getName()); 316 } 317 return; 318 } 319 320 if (debugMode) { 321 if (log.isInfoEnabled()) { 322 log.info("send track for " + eventName + " user : " + wrapper.getUserId() + " with meta : " 323 + metadata.toString()); 324 } 325 } else { 326 if (log.isDebugEnabled()) { 327 log.debug("send track with " + metadata.toString()); 328 } 329 Props eventProperties = new Props(); 330 eventProperties.putAll(wrapper.getMetadata()); 331 if (Framework.isTestModeSet()) { 332 pushForTest("track", wrapper.getUserId(), eventName, eventProperties, buildOptions()); 333 } else { 334 Analytics.getDefaultClient().track(wrapper.getUserId(), eventName, eventProperties, buildOptions()); 335 } 336 } 337 } 338 339 @Override 340 public void flush() { 341 if (!debugMode) { 342 // only flush if Analytics was actually initialized 343 Analytics.flush(); 344 } 345 } 346 347 @Override 348 public Map<String, List<SegmentIOMapper>> getMappers(List<String> events) { 349 Map<String, List<SegmentIOMapper>> targetMappers = new HashMap<String, List<SegmentIOMapper>>(); 350 for (String event : events) { 351 if (event2Mappers.containsKey(event)) { 352 targetMappers.put(event, event2Mappers.get(event)); 353 } 354 } 355 return targetMappers; 356 } 357 358 @Override 359 public Set<String> getMappedEvents() { 360 return event2Mappers.keySet(); 361 } 362 363 @Override 364 public Map<String, List<SegmentIOMapper>> getAllMappers() { 365 return event2Mappers; 366 } 367 368 @Override 369 public SegmentIOUserFilter getUserFilters() { 370 return userFilters; 371 } 372 373 @Override 374 public boolean isDebugMode() { 375 return debugMode; 376 } 377}