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    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}