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}