Ottomata has submitted this change and it was merged. Change subject: Add UAParserUDF from kraken ......................................................................
Add UAParserUDF from kraken This was reviewed over at https://gerrit.wikimedia.org/r/#/c/96738 Added parametized test for most popular user agents Change-Id: I4af37b982809415d3d27fc53b965802e6e6b5b3d --- M pom.xml A refinery-hive/pom.xml A refinery-hive/src/main/java/org/wikimedia/analytics/refinery/hive/UAParserUDF.java A refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFArgumentHandling.java A refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentMostPopular.java A refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentRecognition.java 6 files changed, 720 insertions(+), 0 deletions(-) Approvals: Ottomata: Verified; Looks good to me, approved diff --git a/pom.xml b/pom.xml index 9cbbceb..f93546b 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ <modules> <module>refinery-tools</module> + <module>refinery-hive</module> </modules> <scm> @@ -60,6 +61,10 @@ <id>wmf-mirrored</id> <url>http://archiva.wikimedia.org/repository/mirrored/</url> </repository> + <repository> + <id>wmf-releases</id> + <url>http://archiva.wikimedia.org/repository/releases/</url> + </repository> </repositories> <pluginRepositories> diff --git a/refinery-hive/pom.xml b/refinery-hive/pom.xml new file mode 100644 index 0000000..0fd69d2 --- /dev/null +++ b/refinery-hive/pom.xml @@ -0,0 +1,72 @@ +<project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.wikimedia.analytics.refinery</groupId> + <artifactId>refinery</artifactId> + <version>0.0.1</version> + </parent> + + <groupId>org.wikimedia.analytics.refinery.hive</groupId> + <artifactId>refinery-hive</artifactId> + <name>Wikimedia Analytics Refinery Hive</name> + <packaging>jar</packaging> + + <dependencies> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.hadoop</groupId> + <artifactId>hadoop-common</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.hive</groupId> + <artifactId>hive-exec</artifactId> + <version>0.12.0-cdh5.0.2</version> + </dependency> + + <dependency> + <groupId>ua_parser</groupId> + <artifactId>ua-parser</artifactId> + <version>1.3.0-wmf1</version> + </dependency> + <dependency> + <groupId>com.googlecode.json-simple</groupId> + <artifactId>json-simple</artifactId> + <version>1.1.1</version> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>2.0</version> + <configuration> + <shadedArtifactAttached>false</shadedArtifactAttached> + </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <createDependencyReducedPom>false</createDependencyReducedPom> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/refinery-hive/src/main/java/org/wikimedia/analytics/refinery/hive/UAParserUDF.java b/refinery-hive/src/main/java/org/wikimedia/analytics/refinery/hive/UAParserUDF.java new file mode 100644 index 0000000..7cf97ae --- /dev/null +++ b/refinery-hive/src/main/java/org/wikimedia/analytics/refinery/hive/UAParserUDF.java @@ -0,0 +1,215 @@ +package org.wikimedia.analytics.refinery.hive; + +import org.apache.hadoop.hive.ql.exec.Description; +import org.apache.hadoop.hive.ql.exec.UDFArgumentException; +import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException; +import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException; +import org.apache.hadoop.hive.ql.metadata.HiveException; +import org.apache.hadoop.hive.ql.udf.UDFType; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector.PrimitiveCategory; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.StringObjectInspector; +import org.apache.log4j.Logger; +import ua_parser.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * UDF that parses user agent strings using UA parser and returns a hashmap in + * this form: + * { + * "device_family":"Other", + * "browser_major":"11", + * "os_family":"Windows", + * "os_major":"-", + * "browser_family":"IE", + * "os_minor":"-" + * } + * <p/> + * Records are processed one by one. + * + * NOTE: This UDF was original coded as part of the Kraken repository, and may have not + * received as high of qualitiy code review as we would like. However, it works as is, + * and we want to make this functionality available for use. Please also note that there + * is currently not a process for ensuring that the dependent ua_parser package is up to date + * with the latest user agent classification regexes. + */ + +@UDFType(deterministic = true) +@Description(name = "ua_parser", value = "_FUNC_(UA) - " + + "Returns a map with browser_name, browser_major, device, os_name, os_minor, os_major keys and " + + "the appropriate values for each of them") +public class UAParserUDF extends GenericUDF { + Map<String, String> result = new HashMap<String, String>(); + public CachingParser cachingParser; + private ObjectInspector argumentOI; + + // TODO figure out why not everything is logged to hive.log and some logging + // TODO through log4j stays on the hadoop logs + static final Logger Log = Logger.getLogger(UAParserUDF.class.getName()); + + /** + * The initialize method is called only once during the lifetime of the UDF. + * <p/> + * Method checks for the validity (number, type, etc) + * of the arguments being passed to the UDF. + * It also sets the return type of the result of the UDF, + * in this case the ObjectInspector equivalent of + * Map<String,String> + * + * @param arguments + * @return ObjectInspector Map<String,String> + * @throws UDFArgumentException + */ + @Override + public ObjectInspector initialize(ObjectInspector[] arguments) + throws UDFArgumentException { + + if (arguments.length != 1) { + throw new UDFArgumentLengthException( + "The UAParserUDF takes an array with only 1 element as argument"); + } + + //we are expecting the parameter to be of String type. + ObjectInspector arg = arguments[0]; + int argIndex = 0; + + if (arg.getCategory() != Category.PRIMITIVE) { + throw new UDFArgumentTypeException(argIndex, + "A string argument was expected but an argument of type " + arg.getTypeName() + + " was given."); + + } + + // Now that we have made sure that the argument is of primitive type, we can get the primitive + // category + PrimitiveCategory primitiveCategory = ((PrimitiveObjectInspector) arg) + .getPrimitiveCategory(); + + if (primitiveCategory != PrimitiveCategory.STRING) { + throw new UDFArgumentTypeException(argIndex, + "A string argument was expected but an argument of type " + arg.getTypeName() + + " was given."); + + } + + + try { + cachingParser = new CachingParser(); + } catch (IOException e) { + // no recovery should be possible, log and rethrow + // runtime exception will be logged to stdout by default + Log.error(e.getMessage(), e); + throw new RuntimeException("Failed to instantiate CachingParser"); + } + + argumentOI = arg; + return ObjectInspectorFactory.getStandardMapObjectInspector( + PrimitiveObjectInspectorFactory.javaStringObjectInspector, + PrimitiveObjectInspectorFactory.javaStringObjectInspector); + } + + private final String NA = "-"; + + private String replaceNA(String str) { + final String ret; + if (str == null || str.isEmpty() || str.equals("-")) { + ret = NA; + } else { + ret = str; + } + return ret; + } + + /** + * Takes the actual arguments and returns the result. + * Gets passed the input, does whatever it wants to it, + * and then returns the output. + * <p/> + * The input is accessed using the ObjectInspectors that + * were saved into global variables in the call to initialize() + * <p/> + * This method is called once for every row of data being processed. + * UDFs are called during the map phase of the MapReduce job. + * This means that we have no control over the order in which the + * records get sent to the UDF. + * + * @param arguments + * @return + * @throws HiveException + */ + @SuppressWarnings("unchecked") + @Override + public Object evaluate(DeferredObject[] arguments) throws HiveException { + result.clear(); + + UserAgent browser = null; + Device device = null; + OS os = null; + + try { + if (arguments.length == 1 + && argumentOI != null && arguments[0] != null) { + Client c; + String pattern = ((StringObjectInspector) argumentOI) + .getPrimitiveJavaObject(arguments[0].get()); + + c = cachingParser.parse(pattern); + if (c != null) { + browser = c.userAgent; + device = c.device; + os = c.os; + } + } + } catch (Exception e) { + // catch it all to make sure job does not halt if one record is faulty + // TODO find out why this gets logged to hadoop but not to hive.log + Log.error(e.getMessage(), e); + } + + if (browser != null) { + result.put("browser_family", replaceNA(browser.family)); + result.put("browser_major", replaceNA(browser.major)); + } else { + result.put("browser_family", NA); + result.put("browser_major", NA); + } + + if (device != null) { + result.put("device_family", replaceNA(device.family)); + } else { + result.put("device_family", NA); + } + + if (os != null) { + result.put("os_family", replaceNA(os.family)); + result.put("os_major", replaceNA(os.major)); + result.put("os_minor", replaceNA(os.minor)); + } else { + result.put("os_family", NA); + result.put("os_major", NA); + result.put("os_minor", NA); + } + + return result; + } + + /** + * Returns string representation of the function. + * Will be printed when an error happens. + * * + */ + @Override + public String getDisplayString(String[] arguments) { + assert (arguments.length == 1); + return "user_agent_parser(" + arguments[0] + ")"; + + } +} diff --git a/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFArgumentHandling.java b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFArgumentHandling.java new file mode 100644 index 0000000..9574094 --- /dev/null +++ b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFArgumentHandling.java @@ -0,0 +1,40 @@ +package org.wikimedia.analytics.refinery.hive; + +import org.apache.hadoop.hive.ql.exec.UDFArgumentLengthException; +import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException; +import org.apache.hadoop.hive.ql.metadata.HiveException; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory; +import org.junit.Test; + +import java.util.HashMap; + +public class TestUAParserUDFArgumentHandling { + + + @Test(expected = UDFArgumentLengthException.class) + public void testBadNumberOfArguments() throws HiveException { + + ObjectInspector value1 = PrimitiveObjectInspectorFactory.javaStringObjectInspector; + ObjectInspector value2 = PrimitiveObjectInspectorFactory.javaStringObjectInspector; + ObjectInspector[] initArguments = new ObjectInspector[]{value1, value2}; + UAParserUDF uaParserUDF = new UAParserUDF(); + uaParserUDF.initialize(initArguments); + + } + + @Test(expected = UDFArgumentTypeException.class) + public void testWrongTypeOfArguments() throws HiveException { + + ObjectInspector value1 = PrimitiveObjectInspectorFactory.javaIntObjectInspector; + ObjectInspector[] initArguments = new ObjectInspector[]{value1}; + UAParserUDF uaParserUDF = new UAParserUDF(); + uaParserUDF.initialize(initArguments); + + } + + +} + + + diff --git a/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentMostPopular.java b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentMostPopular.java new file mode 100644 index 0000000..8e9e6df --- /dev/null +++ b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentMostPopular.java @@ -0,0 +1,217 @@ +package org.wikimedia.analytics.refinery.hive; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Collection; +import java.util.Arrays; + +import org.apache.hadoop.hive.ql.metadata.HiveException; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredJavaObject; + +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory; +import org.junit.Before; +import org.junit.After; +import org.junit.Test; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runner.RunWith; +import org.json.simple.parser.JSONParser; +import org.json.simple.JSONObject; +import org.json.simple.parser.ParseException; + + +/** + * We test the most popular browser + device combos (from sampled logs) + * and the ua parser reporting on on those. + * <p/> + * Test failing will indicate than the newer version of ua parser + * is significantly different from the prior one. + */ +@RunWith(Parameterized.class) +public class TestUAParserUDFUserAgentMostPopular { + ObjectInspector[] initArguments = null; + UAParserUDF uaParserUDF = null; + JSONParser parser = null; + + @Before + public void setUp() throws HiveException { + ObjectInspector valueOI = PrimitiveObjectInspectorFactory.javaStringObjectInspector; + initArguments = new ObjectInspector[]{valueOI}; + uaParserUDF = new UAParserUDF(); + uaParserUDF.initialize(initArguments); + parser = new JSONParser(); + + } + + @After + public void tearDown() throws HiveException { + + try { + uaParserUDF.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + @Parameters + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"8\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"GMozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.102 Mobile Safari/537.36", "{\"os_minor\": \"4\", \"os_major\": \"4\", \"device_family\": \"Samsung GT-I9505\", \"os_family\": \"Android\", \"browser_major\": \"38\", \"browser_family\": \"Chrome Mobile\"}"}, + {"Mozilla/5.0 (compatible; YoudaoBot/1.0; http://www.youdao.com/help/webmaster/spider/; )", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Spider\", \"os_family\": \"Other\", \"browser_major\": \"-\", \"browser_family\": \"Other\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/6.2 Safari/537.85.10", "{\"os_minor\": \"8\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"6\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"36\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) CriOS/38.0.2125.59 Mobile/11D257 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"38\", \"browser_family\": \"Chrome Mobile iOS\"}"}, + {"Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.17", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows XP\", \"browser_major\": \"12\", \"browser_family\": \"Opera\"}"}, + {"Mozilla/5.0 (Linux; Android 4.4.4; Nexus 5 Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.102 Mobile Safari/537.36", "{\"os_minor\": \"4\", \"os_major\": \"4\", \"device_family\": \"Nexus 5\", \"os_family\": \"Android\", \"browser_major\": \"38\", \"browser_family\": \"Chrome Mobile\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53", "{\"os_minor\": \"0\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36 OPR/25.0.1614.50", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"25\", \"browser_family\": \"Opera\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25", "{\"os_minor\": \"1\", \"os_major\": \"6\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"6\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"7\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"35\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/38.0.2125.59 Mobile/12A405 Safari/600.1.4", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"38\", \"browser_family\": \"Chrome Mobile iOS\"}"}, + {"Mozilla/5.0 (Android; Mobile; rv:33.0) Gecko/33.0 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Android\", \"browser_major\": \"33\", \"browser_family\": \"Firefox Mobile\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"AppleDictionaryService/208", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"-\", \"browser_family\": \"Other\"}"}, + {"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows Vista\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3", "{\"os_minor\": \"1\", \"os_major\": \"5\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"5\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (en-us) AppleWebKit/534.14 (KHTML, like Gecko; Google Wireless Transcoder) Chrome/9.0.597 Safari/534.14", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"9\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 YaBrowser/14.8.1985.12084 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"14\", \"browser_family\": \"Yandex Browser\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A365 Safari/600.1.4", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"MediaWiki/1.25wmf3", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"-\", \"browser_family\": \"Other\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"11\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"6\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.17", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"12\", \"browser_family\": \"Opera\"}"}, + {"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Ubuntu\", \"browser_major\": \"33\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B410 Safari/600.1.4", "{\"os_minor\": \"1\", \"os_major\": \"8\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36 OPR/25.0.1614.50", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"25\", \"browser_family\": \"Opera\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_6 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B651 Safari/9537.53", "{\"os_minor\": \"0\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "{\"os_minor\": \"0\", \"os_major\": \"6\", \"device_family\": \"Spider\", \"os_family\": \"iOS\", \"browser_major\": \"2\", \"browser_family\": \"Googlebot\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"31\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/6.1.6 Safari/537.78.2", "{\"os_minor\": \"7\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"6\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 6_1_3 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25", "{\"os_minor\": \"1\", \"os_major\": \"6\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"6\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8\", \"browser_major\": \"10\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53", "{\"os_minor\": \"0\", \"os_major\": \"7\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 YaBrowser/14.8.1985.12084 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"14\", \"browser_family\": \"Yandex Browser\"}"}, + {"Mozilla/5.0 (Windows NT 6.0; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows Vista\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/4.2.2.38484 Mobile/12A405 Safari/9537.53", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"7\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/7.0.6 Safari/537.78.2", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"7\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) GSA/4.2.2.38484 Mobile/11D257 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows Vista\", \"browser_major\": \"9\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Windows NT 6.2; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Spider\", \"os_family\": \"Other\", \"browser_major\": \"2\", \"browser_family\": \"Googlebot\"}"}, + {"NativeHost", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"-\", \"browser_family\": \"Other\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.78.2 (KHTML, like Gecko) Version/7.0.6 Safari/537.78.2", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"7\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36 OPR/25.0.1614.50", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"25\", \"browser_family\": \"Opera\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"33\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.59.10 (KHTML, like Gecko) Version/5.1.9 Safari/534.59.10", "{\"os_minor\": \"6\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"5\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"10\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"10\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows XP\", \"browser_major\": \"33\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows Vista\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"1\", \"browser_family\": \"FacebookBot\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; rv:33.0) Gecko/20100101 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"33\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A365 Safari/600.1.4", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53", "{\"os_minor\": \"0\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4", "{\"os_minor\": \"1\", \"os_major\": \"8\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"10\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows XP\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"-", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Other\", \"browser_major\": \"-\", \"browser_family\": \"Other\"}"}, + {"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Spider\", \"os_family\": \"Other\", \"browser_major\": \"2\", \"browser_family\": \"bingbot\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"11\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25", "{\"os_minor\": \"10\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"8\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"9\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/600.1.17 (KHTML, like Gecko) Version/7.1 Safari/537.85.10", "{\"os_minor\": \"9\", \"os_major\": \"10\", \"device_family\": \"Other\", \"os_family\": \"Mac OS X\", \"browser_major\": \"7\", \"browser_family\": \"Safari\"}"}, + {"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"9\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPad\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 5.1; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows XP\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"33\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"11\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows XP\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"37\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 8_0_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A405 Safari/600.1.4", "{\"os_minor\": \"0\", \"os_major\": \"8\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"8\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 8.1\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53", "{\"os_minor\": \"1\", \"os_major\": \"7\", \"device_family\": \"iPhone\", \"os_family\": \"iOS\", \"browser_major\": \"7\", \"browser_family\": \"Mobile Safari\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"11\", \"browser_family\": \"IE\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"32\", \"browser_family\": \"Firefox\"}"}, + {"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36", "{\"os_minor\": \"-\", \"os_major\": \"-\", \"device_family\": \"Other\", \"os_family\": \"Windows 7\", \"browser_major\": \"38\", \"browser_family\": \"Chrome\"}"}, + + }); + } + + private String fInput; + + private String fExpected; + + + public TestUAParserUDFUserAgentMostPopular(String input, String expected) { + fInput = input; + fExpected = expected; + } + + + @Test + public void testMatchingOfMostPopularUA() throws HiveException, ParseException { + + DeferredJavaObject ua = new DeferredJavaObject(fInput); + + // decode expected output and turn it into an object + Object obj = parser.parse(fExpected); + JSONObject expected_ua = (JSONObject) obj; + + DeferredObject[] args1 = {ua}; + HashMap<String, String> computed_ua = (HashMap<String, String>) uaParserUDF + .evaluate(args1); + + assertEquals("OS name check", expected_ua.get("os_family"), + computed_ua.get("os_family")); + + assertEquals("OS major version check", expected_ua.get("os_major"), + computed_ua.get("os_major")); + + assertEquals("OS minor version check", expected_ua.get("os_minor"), + computed_ua.get("os_minor")); + + assertEquals("browser check", expected_ua.get("browser_family"), + computed_ua.get("browser_family")); + + assertEquals("browser major version check", expected_ua.get("browser_major"), + computed_ua.get("browser_major")); + + assertEquals("device check", expected_ua.get("device_family"), + computed_ua.get("device_family")); + + } + +} + + + diff --git a/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentRecognition.java b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentRecognition.java new file mode 100644 index 0000000..6002b07 --- /dev/null +++ b/refinery-hive/src/test/java/org/wikimedia/analytics/refinery/hive/TestUAParserUDFUserAgentRecognition.java @@ -0,0 +1,171 @@ +package org.wikimedia.analytics.refinery.hive; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.HashMap; +import org.apache.hadoop.hive.ql.metadata.HiveException; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredJavaObject; + +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory; +import org.junit.Before; +import org.junit.After; +import org.junit.Test; + +public class TestUAParserUDFUserAgentRecognition { + ObjectInspector[] initArguments = null; + UAParserUDF uaParserUDF = null; + + @Before + public void setUp() throws HiveException { + ObjectInspector valueOI = PrimitiveObjectInspectorFactory.javaStringObjectInspector; + initArguments = new ObjectInspector[]{valueOI}; + uaParserUDF = new UAParserUDF(); + uaParserUDF.initialize(initArguments); + + } + + @After + public void tearDown() throws HiveException { + + try { + uaParserUDF.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + @Test + public void testHappyCase() throws HiveException { + + DeferredJavaObject ua1 = new DeferredJavaObject( + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:19.0) Gecko/20100101 Firefox/19.0"); + DeferredJavaObject ua2 = new DeferredJavaObject( + "Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405"); + DeferredJavaObject ua3 = new DeferredJavaObject( + "Mozilla/5.0 (iPad; CPU OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53"); + + + DeferredObject[] args1 = { ua1 }; + HashMap<String, String> evaled = (HashMap<String, String>) uaParserUDF + .evaluate(args1); + assertEquals("OS name check", (new String("Ubuntu")), + evaled.get("os_family").toString()); + assertEquals("Browser name check", (new String("Firefox")), + evaled.get("browser_family").toString()); + + + DeferredObject[] args2 = { ua2 }; + evaled = (HashMap<String, String>) uaParserUDF + .evaluate(args2); + assertEquals("OS name check", (new String("iOS")), + evaled.get("os_family").toString()); + + assertEquals("Browser name check", (new String("Mobile Safari")), + evaled.get("browser_family").toString()); + + + DeferredObject[] args3 = { ua3 }; + evaled = (HashMap<String, String>) uaParserUDF + .evaluate(args3); + assertEquals("OS name check", (new String("iOS")), + evaled.get("os_family").toString()); + assertEquals("Browser name check", (new String("Mobile Safari")), + evaled.get("browser_family").toString()); + + + + + + } + + /** + * Tests what we return when browser is empty. + * + * UA parser will return this as "browser obj" for an empty user agent string: + * { + * user_agent: {family: "Other", major: null, minor: null, patch: null}, + * os: {family: "Other", major: null, minor: null, patch: null, patch_minor: null}, + * device: {family: "Other"} + * } + * UDFs returns something like the following: + * { + * "device_family":"Other", + * "browser_major":"-", + * "os_family":"Other", + * "os_major":"-", + * "browser_family":"Other", + * "os_minor":"-" + * } + **/ + @Test + public void testEmptyUA() throws HiveException { + DeferredJavaObject ua1 = new DeferredJavaObject(""); + + DeferredObject[] args = { ua1 }; + + HashMap<String, String> evaled = (HashMap<String, String>) uaParserUDF + .evaluate(args); + + String resultOSName = evaled.get("os_family"); + String resultBrowserName = evaled.get("browser_family"); + String resultOsMinor = evaled.get("os_minor"); + assertEquals("OS name check", (new String("Other")), + resultOSName.toString()); + assertEquals("Browser name check", (new String("Other")), + resultBrowserName.toString()); + + assertEquals("OS minor", (new String("-")), + resultOsMinor.toString()); + } + + /** + * Tests what we return when browser is null + * Is this right? Maybe we should return just null + * + * UA parser will return this as "browser obj" for an empty user agent string: + * { + * user_agent: {family: "Other", major: null, minor: null, patch: null}, + * os: {family: "Other", major: null, minor: null, patch: null, patch_minor: null}, + * device: {family: "Other"} + * } + * UDFs returns something like the following: + * { + * "device_family":"Other", + * "browser_major":"-", + * "os_family":"Other", + * "os_major":"-", + * "browser_family":"Other", + * "os_minor":"-" + * } + **/ + public void testHandlingOfNulls() throws HiveException { + + DeferredJavaObject ua1 = new DeferredJavaObject(null); + + DeferredObject[] args = { ua1 }; + + HashMap<String, String> evaled = (HashMap<String, String>) uaParserUDF + .evaluate(args); + + String resultOSName = evaled.get("os_family"); + String resultBrowserName = evaled.get("browser_family"); + String resultOsMinor = evaled.get("os_minor"); + assertEquals("OS name check", (new String("Other")), + resultOSName.toString()); + assertEquals("Browser name check", (new String("Other")), + resultBrowserName.toString()); + + assertEquals("OS minor", (new String("-")), + resultOsMinor.toString()); + + } + +} + + + -- To view, visit https://gerrit.wikimedia.org/r/166142 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I4af37b982809415d3d27fc53b965802e6e6b5b3d Gerrit-PatchSet: 14 Gerrit-Project: analytics/refinery/source Gerrit-Branch: master Gerrit-Owner: Ottomata <[email protected]> Gerrit-Reviewer: Nuria <[email protected]> Gerrit-Reviewer: Ottomata <[email protected]> Gerrit-Reviewer: QChris <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
