embulk-input-mixpanel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +2 -0
- data/LICENSE +13 -0
- data/README.md +53 -0
- data/Rakefile +60 -0
- data/embulk-input-mixpanel.gemspec +25 -0
- data/gemfiles/embulk-0.6.16 +4 -0
- data/gemfiles/embulk-0.6.17 +4 -0
- data/gemfiles/embulk-latest +2 -0
- data/lib/embulk/input/mixpanel.rb +165 -0
- data/lib/embulk/input/mixpanel_api/client.rb +62 -0
- data/test/embulk/input/mixpanel_api/test_client.rb +130 -0
- data/test/embulk/input/test_mixpanel.rb +175 -0
- data/test/prepare_embulk.rb +10 -0
- data/test/run-test.rb +18 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d553d9b694f3b3150c2c634f1f4b334e7c2162e9
|
4
|
+
data.tar.gz: 89480ccc7dc13ace59cb88769af0263b0c078c72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60063447a33ad85b84d33bb01d85ad757c3bcc0494908f30a9c8c78e8fb5df17049c27833b1ce6acccf52358b6575b036ebdbd770fbff4e38558ecdd90734120
|
7
|
+
data.tar.gz: d98177f66557bec6a845749bf93ac69a4b4b8bbbbf63695222f945d37b1d6832432eb58c8448b354018c819e676d5fcf48409b723a32fd488291fd0b724f6bd6
|
data/.travis.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- jruby-19mode
|
4
|
+
addons:
|
5
|
+
code_climate:
|
6
|
+
repo_token:
|
7
|
+
secure: "opE/ZhRzsEU2Fn6YEnItMD/rMs3O2OHVYQQ7Ly0dAkIc9ZrVMa//ogt4k7h0QGZUettRVV7kawCtRzde+QbOmezTRwWqQJ7Mi6D6qfwlYMz+D9FcufHaCJyLy+dYeBuQqr1d6n/3gVqVGh8MAMGHipYodudCub38DQ1sVWcCMDNNo4PMRFLS0pS839SC80HAS7tutOeaeohRc+Ct5yoY2ZDoTuGtNeJhwqmDAo13RwvVu9aZw97EZPvt8UcaW5oYDnx47kDpKi4XGrTPaWpSm/IitwW11FF+Kevt0RpUS0uVWqg4/6xTjDg++ETG+94ePYrOZGF4ne/CPtC0AtaWF1jgSlL9iu4IR/Awt+2BqawKzCnoSRgCGIHRZApErC3KacadJBaPCrKwf4xxxqRXex4lXcptKRygkG4ic2r+MblyPPwIsc1Wb1QYCeVjIEGWzOgKVEUpT8qN0DAj6KQe/HuyuXKE/FiPIRfkJkRY5oGCNZtzeCzXlC+IqhZYCg6HwcZuk3CbcxRrg5VFKDlL0VWacD/FQoGgfTp3SUmaL6NMcCKSrL0vjEgx98/yM9rsGZlZOU9ioN93PQVgqpI5dl+nm9vKkFyuzW07nNM+/6PNdHbbZBY1OLNd3RSpWVC9TDtZ3Q2gAx25+31TDzD2/3yjJjfdtwIH3bdyrO44MK8="
|
8
|
+
notifications:
|
9
|
+
slack:
|
10
|
+
secure: cAAHcb/HdJK6elr6gfxlotUJdZys8nA/BUb1rKVt7T7EShOjqNOwHo0vZBG5fXQEbUJjHCFAl8tH1N8wz+KLoVErlMmD2OgBKGgvgm3fddkIgGRRvxe8yB0yEFy5yuvfgqJDlwPHZxbwj2Ddb8uTaLd8yzmRzGWNwLyRQTDaQbaKBaB+emP2t/1AilQHyItN02NuW3qcEGr7Hzg+IsKabDRzhMi89RV0xk2eP5iWTYW76ULbbfP3NqBpcVLskAqVQd1YOFUSEb2fdzRj7UuLUUp3C0Kb5gKfXPHb97XuMqwyviakTr02omdaM82NvB8GKn7QKBbcwA1Iz1euinxQQ7MTzJe8wLMH8q205lTpl96CANKwIxrk/HestgEboq97xSp5Wc1Qn1ynnrLOvBYW7EG/e6exguojQmkLkV9P6LW9g4nYsR8UEjyuRd252xch4+b4M3lxtlAXtpWguPfm5ZKhICBsJMlYm6//q1cBlNUOzR0cufCHDTNdlB/YLynvdg1ykFISYARP+5sV2YM9rD/1Rs/396qYr0mNbCh83P9GlZ5NZtWLQePJroHe3iQ18X3YHEcb50OTfRmKNoXTldQBD9iKNkwlFpWfruVdBWT+JRM77+yIV9ORjY7UE4ZtH8HxAg4bX+ey+C5axgxQk569h3CdLmVapFt6wovCSVk=
|
11
|
+
jdk:
|
12
|
+
- oraclejdk8
|
13
|
+
gemfile:
|
14
|
+
- gemfiles/embulk-0.6.16
|
15
|
+
- gemfiles/embulk-0.6.17
|
16
|
+
- gemfiles/embulk-latest
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2015 Everyleaf Corporation
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/treasure-data/embulk-input-mixpanel.svg?branch=master)](https://travis-ci.org/treasure-data/embulk-input-mixpanel)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/badges/gpa.svg)](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/badges/coverage.svg)](https://codeclimate.com/github/treasure-data/embulk-input-mixpanel/coverage)
|
4
|
+
|
5
|
+
# Mixpanel input plugin for Embulk
|
6
|
+
|
7
|
+
embulk-input-mixpanel is the Embulk input plugin for [Mixpanel](https://mixpanel.com).
|
8
|
+
|
9
|
+
## Overview
|
10
|
+
|
11
|
+
Required Embulk version >= 0.6.16.
|
12
|
+
|
13
|
+
* **Plugin type**: input
|
14
|
+
* **Resume supported**: no
|
15
|
+
* **Cleanup supported**: no
|
16
|
+
* **Guess supported**: yes
|
17
|
+
|
18
|
+
## Setup
|
19
|
+
|
20
|
+
### How to get API configuration
|
21
|
+
|
22
|
+
This plugin uses API key and API secret for target project. Before you make your config.yml, you should get API key and API secret in mixpanel website.
|
23
|
+
|
24
|
+
For API configuration, you should log in mixpanel website, and click "Account" at the header. When you select "Projects" panel, you can get "API Key" and "API Secret" for each project.
|
25
|
+
|
26
|
+
### Configuration
|
27
|
+
|
28
|
+
- **api_key**: project API Key (string, required)
|
29
|
+
- **api_secret**: project API Secret (string, required)
|
30
|
+
- **timezone**: project timezone(string, required)
|
31
|
+
- **from_date**: From date to export (string, required)
|
32
|
+
- **to_date**: To date to export (string, required)
|
33
|
+
- **event**: The event or events to filter data (array, optional, default: nil)
|
34
|
+
- **where**: Expression to filter data (c.f. https://mixpanel.com/docs/api-documentation/data-export-api#segmentation-expressions) (string, optional, default: nil)
|
35
|
+
- **bucket**:The data backet to filter data (string, optional, default: nil)
|
36
|
+
|
37
|
+
## Example
|
38
|
+
|
39
|
+
```yaml
|
40
|
+
in:
|
41
|
+
type: mixpanel
|
42
|
+
api_key: "API_KEY"
|
43
|
+
api_secret: "API_SECRET"
|
44
|
+
timezone: "US/Pacific"
|
45
|
+
from_date: "2015-07-19"
|
46
|
+
to_date: "2015-07-20"
|
47
|
+
```
|
48
|
+
|
49
|
+
## Run test
|
50
|
+
|
51
|
+
```
|
52
|
+
$ rake
|
53
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
task default: :test
|
4
|
+
|
5
|
+
desc "Run tests"
|
6
|
+
task :test do
|
7
|
+
ruby("test/run-test.rb", "--use-color=yes", "--collector=dir")
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :release do
|
11
|
+
desc "Add header of now version release to ChangeLog and bump up version"
|
12
|
+
task :prepare do
|
13
|
+
root_dir = Pathname.new(File.expand_path("../", __FILE__))
|
14
|
+
changelog_file = root_dir.join("CHANGELOG.md")
|
15
|
+
gemspec_file = root_dir.join("embulk-input-mixpanel.gemspec")
|
16
|
+
|
17
|
+
system("git fetch origin")
|
18
|
+
|
19
|
+
# detect merged PR
|
20
|
+
old_version = gemspec_file.read[/spec\.version += *"([0-9]+\.[0-9]+\.[0-9]+)"/, 1]
|
21
|
+
pr_numbers = `git log v#{old_version}..origin/master --oneline`.scan(/#[0-9]+/)
|
22
|
+
|
23
|
+
if !$?.success? || pr_numbers.empty?
|
24
|
+
puts "Detecting PR failed. Please confirm if any PR were merged after the latest release."
|
25
|
+
exit(false)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Generate new version
|
29
|
+
major, minor, patch = old_version.split(".").map(&:to_i)
|
30
|
+
new_version = "#{major}.#{minor}.#{patch + 1}"
|
31
|
+
|
32
|
+
# Update ChangeLog
|
33
|
+
pr_descriptions = pr_numbers.map do |number|
|
34
|
+
body = open("https://api.github.com/repos/treasure-data/embulk-input-mixpanel/issues/#{number.gsub("#", "")}").read
|
35
|
+
payload = JSON.parse(body)
|
36
|
+
"* [] #{payload["title"]} [#{number}](https://github.com/treasure-data/embulk-input-mixpanel/pull/#{number.gsub('#', '')})"
|
37
|
+
end.join("\n")
|
38
|
+
|
39
|
+
new_changelog = <<-HEADER
|
40
|
+
## #{new_version} - #{Time.now.strftime("%Y-%m-%d")}
|
41
|
+
#{pr_descriptions}
|
42
|
+
|
43
|
+
#{changelog_file.read.chomp}
|
44
|
+
HEADER
|
45
|
+
|
46
|
+
File.open(changelog_file, "w") {|f| f.write(new_changelog) }
|
47
|
+
|
48
|
+
# Update version.rb
|
49
|
+
old_content = gemspec_file.read
|
50
|
+
File.open(gemspec_file, "w") do |f|
|
51
|
+
f.write old_content.gsub(/(spec\.version += *)".*?"/, %Q!\\1"#{new_version}"!)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Update Gemfile.lock
|
55
|
+
system("bundle install")
|
56
|
+
|
57
|
+
puts "ChangeLog, version and Gemfile.lock were updated. New version is #{new_version}."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
Gem::Specification.new do |spec|
|
3
|
+
spec.name = "embulk-input-mixpanel"
|
4
|
+
spec.version = "0.1.0"
|
5
|
+
spec.authors = ["yoshihara", "uu59"]
|
6
|
+
spec.summary = "Mixpanel input plugin for Embulk"
|
7
|
+
spec.description = "Loads records from Mixpanel."
|
8
|
+
spec.email = ["h.yoshihara@everyleaf.com", "k@uu59.org"]
|
9
|
+
spec.licenses = ["Apache2"]
|
10
|
+
spec.homepage = "https://github.com/treasure-data/embulk-input-mixpanel"
|
11
|
+
|
12
|
+
spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"]
|
13
|
+
spec.test_files = spec.files.grep(%r{^(test|spec)/})
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
|
16
|
+
spec.add_dependency 'httpclient'
|
17
|
+
spec.add_dependency 'tzinfo'
|
18
|
+
spec.add_development_dependency 'bundler', ['~> 1.0']
|
19
|
+
spec.add_development_dependency 'rake', ['>= 10.0']
|
20
|
+
spec.add_development_dependency 'embulk', ['>= 0.6.16', '< 1.0']
|
21
|
+
spec.add_development_dependency 'pry'
|
22
|
+
spec.add_development_dependency 'test-unit'
|
23
|
+
spec.add_development_dependency 'test-unit-rr'
|
24
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
25
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require "tzinfo"
|
2
|
+
require "embulk/input/mixpanel_api/client"
|
3
|
+
|
4
|
+
module Embulk
|
5
|
+
module Input
|
6
|
+
|
7
|
+
class Mixpanel < InputPlugin
|
8
|
+
Plugin.register_input("mixpanel", self)
|
9
|
+
|
10
|
+
GUESS_RECORDS_COUNT = 10
|
11
|
+
|
12
|
+
# NOTE: It takes long time to fetch data between from_date to
|
13
|
+
# to_date by one API request. So this plugin fetches data
|
14
|
+
# between each 7 (SLICE_DAYS_COUNT) days.
|
15
|
+
SLICE_DAYS_COUNT = 7
|
16
|
+
|
17
|
+
def self.transaction(config, &control)
|
18
|
+
task = {}
|
19
|
+
|
20
|
+
task[:params] = export_params(config)
|
21
|
+
|
22
|
+
from_date = config.param(:from_date, :string)
|
23
|
+
to_date = config.param(:to_date, :string)
|
24
|
+
dates = Date.parse(from_date)..Date.parse(to_date)
|
25
|
+
task[:dates] = dates.map {|date| date.to_s}
|
26
|
+
|
27
|
+
task[:api_key] = config.param(:api_key, :string)
|
28
|
+
task[:api_secret] = config.param(:api_secret, :string)
|
29
|
+
task[:timezone] = config.param(:timezone, :string)
|
30
|
+
|
31
|
+
begin
|
32
|
+
# raises exception if timezone is invalid string
|
33
|
+
TZInfo::Timezone.get(task[:timezone])
|
34
|
+
rescue => e
|
35
|
+
Embulk.logger.error "'#{task[:timezone]}' is invalid timezone"
|
36
|
+
raise e
|
37
|
+
end
|
38
|
+
|
39
|
+
columns = []
|
40
|
+
task[:schema] = config.param(:columns, :array)
|
41
|
+
task[:schema].each do |column|
|
42
|
+
name = column["name"]
|
43
|
+
type = column["type"].to_sym
|
44
|
+
|
45
|
+
columns << Column.new(nil, name, type, column["format"])
|
46
|
+
end
|
47
|
+
|
48
|
+
resume(task, columns, 1, &control)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.resume(task, columns, count, &control)
|
52
|
+
commit_reports = yield(task, columns, count)
|
53
|
+
|
54
|
+
next_config_diff = {}
|
55
|
+
return next_config_diff
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.guess(config)
|
59
|
+
client = MixpanelApi::Client.new(config.param(:api_key, :string), config.param(:api_secret, :string))
|
60
|
+
|
61
|
+
from_date = config.param(:from_date, :string)
|
62
|
+
# NOTE: It should have 7 days beteen from_date and to_date
|
63
|
+
to_date = (Date.parse(from_date) + SLICE_DAYS_COUNT - 1).to_s
|
64
|
+
|
65
|
+
params = export_params(config)
|
66
|
+
params = params.merge(
|
67
|
+
from_date: from_date,
|
68
|
+
to_date: to_date,
|
69
|
+
)
|
70
|
+
|
71
|
+
records = client.export(params)
|
72
|
+
sample_records = records.first(GUESS_RECORDS_COUNT)
|
73
|
+
properties = Guess::SchemaGuess.from_hash_records(sample_records.map{|r| r["properties"]})
|
74
|
+
columns = properties.map do |col|
|
75
|
+
result = {
|
76
|
+
name: col.name,
|
77
|
+
type: col.type,
|
78
|
+
}
|
79
|
+
result[:format] = col.format if col.format
|
80
|
+
result
|
81
|
+
end
|
82
|
+
columns.unshift(name: "event", type: :string)
|
83
|
+
return {"columns" => columns}
|
84
|
+
end
|
85
|
+
|
86
|
+
def init
|
87
|
+
@api_key = task[:api_key]
|
88
|
+
@api_secret = task[:api_secret]
|
89
|
+
@params = task[:params]
|
90
|
+
@timezone = task[:timezone]
|
91
|
+
@schema = task[:schema]
|
92
|
+
@dates = task[:dates]
|
93
|
+
end
|
94
|
+
|
95
|
+
def run
|
96
|
+
client = MixpanelApi::Client.new(@api_key, @api_secret)
|
97
|
+
@dates.each_slice(SLICE_DAYS_COUNT) do |dates|
|
98
|
+
from_date = dates.first
|
99
|
+
to_date = dates.last
|
100
|
+
Embulk.logger.info "Fetching data from #{from_date} to #{to_date} ..."
|
101
|
+
|
102
|
+
params = @params.merge(
|
103
|
+
"from_date" => from_date,
|
104
|
+
"to_date" => to_date
|
105
|
+
)
|
106
|
+
|
107
|
+
records = client.export(params)
|
108
|
+
|
109
|
+
records.each do |record|
|
110
|
+
values = @schema.map do |column|
|
111
|
+
case column["name"]
|
112
|
+
when "event"
|
113
|
+
record["event"]
|
114
|
+
when "time"
|
115
|
+
time = record["properties"]["time"]
|
116
|
+
adjust_timezone(time)
|
117
|
+
else
|
118
|
+
record["properties"][column["name"]]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
page_builder.add(values)
|
122
|
+
end
|
123
|
+
|
124
|
+
break if preview?
|
125
|
+
end
|
126
|
+
|
127
|
+
page_builder.finish
|
128
|
+
|
129
|
+
commit_report = {}
|
130
|
+
return commit_report
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def adjust_timezone(epoch)
|
136
|
+
# Adjust timezone offset to get UTC time
|
137
|
+
# c.f. https://mixpanel.com/docs/api-documentation/exporting-raw-data-you-inserted-into-mixpanel#export
|
138
|
+
tz = TZInfo::Timezone.get(@timezone)
|
139
|
+
offset = tz.period_for_local(epoch, true).offset.utc_offset
|
140
|
+
epoch - offset
|
141
|
+
end
|
142
|
+
|
143
|
+
def preview?
|
144
|
+
begin
|
145
|
+
org.embulk.spi.Exec.isPreview()
|
146
|
+
rescue java.lang.NullPointerException => e
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.export_params(config)
|
152
|
+
event = config.param(:event, :array, default: nil)
|
153
|
+
event = event.nil? ? nil : event.to_json
|
154
|
+
|
155
|
+
{
|
156
|
+
api_key: config.param(:api_key, :string),
|
157
|
+
event: event,
|
158
|
+
where: config.param(:where, :string, default: nil),
|
159
|
+
bucket: config.param(:bucket, :string, default: nil),
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "digest/md5"
|
3
|
+
require "json"
|
4
|
+
require "httpclient"
|
5
|
+
|
6
|
+
module Embulk
|
7
|
+
module Input
|
8
|
+
module MixpanelApi
|
9
|
+
class Client
|
10
|
+
ENDPOINT_EXPORT = "https://data.mixpanel.com/api/2.0/export/".freeze
|
11
|
+
TIMEOUT_SECONDS = 3600
|
12
|
+
|
13
|
+
def initialize(api_key, api_secret)
|
14
|
+
@api_key = api_key
|
15
|
+
@api_secret = api_secret
|
16
|
+
end
|
17
|
+
|
18
|
+
def export(params = {})
|
19
|
+
# https://mixpanel.com/docs/api-documentation/exporting-raw-data-you-inserted-into-mixpanel
|
20
|
+
params[:expire] ||= Time.now.to_i + TIMEOUT_SECONDS
|
21
|
+
params[:sig] = signature(params)
|
22
|
+
response = httpclient.get(ENDPOINT_EXPORT, params)
|
23
|
+
|
24
|
+
if response.code >= 400
|
25
|
+
Embulk.logger.error response.body
|
26
|
+
return Enumerator.new{ }
|
27
|
+
end
|
28
|
+
|
29
|
+
Enumerator.new do |y|
|
30
|
+
response.body.lines.each do |json|
|
31
|
+
y << JSON.parse(json)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def signature(params)
|
39
|
+
# https://mixpanel.com/docs/api-documentation/data-export-api#auth-implementation
|
40
|
+
sorted_keys = params.keys.map(&:to_s).sort
|
41
|
+
signature = sorted_keys.inject("") do |sig, key|
|
42
|
+
value = params[key] || params[key.to_sym]
|
43
|
+
next sig unless value
|
44
|
+
sig << "#{key}=#{value}"
|
45
|
+
end
|
46
|
+
|
47
|
+
Digest::MD5.hexdigest(signature + @api_secret)
|
48
|
+
end
|
49
|
+
|
50
|
+
def httpclient
|
51
|
+
@client ||=
|
52
|
+
begin
|
53
|
+
client = HTTPClient.new
|
54
|
+
client.receive_timeout = TIMEOUT_SECONDS
|
55
|
+
client.default_header = {Accept: "application/json; charset=UTF-8"}
|
56
|
+
client
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require "embulk/input/mixpanel_api/client"
|
2
|
+
|
3
|
+
module Embulk
|
4
|
+
module Input
|
5
|
+
module MixpanelApi
|
6
|
+
class ClientTest < Test::Unit::TestCase
|
7
|
+
API_KEY = "api_key".freeze
|
8
|
+
API_SECRET = "api_secret".freeze
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@client = Client.new(API_KEY, API_SECRET)
|
12
|
+
end
|
13
|
+
|
14
|
+
# NOTE: Client#signature is private method but this value
|
15
|
+
# can't be checked via other methods.
|
16
|
+
def test_signature
|
17
|
+
now = Time.parse("2015-07-22 00:00:00")
|
18
|
+
stub(Time).now { now }
|
19
|
+
|
20
|
+
params = {
|
21
|
+
string: "string",
|
22
|
+
array: ["elem1", "elem2"],
|
23
|
+
}
|
24
|
+
expected = "4be4a4f92f57e12b543a2a5f2f5897b6"
|
25
|
+
|
26
|
+
assert_equal(expected, @client.__send__(:signature, params))
|
27
|
+
end
|
28
|
+
|
29
|
+
class ExportTest < self
|
30
|
+
def setup
|
31
|
+
super
|
32
|
+
|
33
|
+
@httpclient = HTTPClient.new
|
34
|
+
stub(@client).httpclient { @httpclient }
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_response_class
|
38
|
+
stub_response(success_response)
|
39
|
+
|
40
|
+
actual = @client.export(params)
|
41
|
+
|
42
|
+
assert_equal(Enumerator, actual.class)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_http_request
|
46
|
+
mock(@httpclient).get(Client::ENDPOINT_EXPORT, params) do
|
47
|
+
success_response
|
48
|
+
end
|
49
|
+
|
50
|
+
@client.export(params)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_success
|
54
|
+
stub_response(success_response)
|
55
|
+
|
56
|
+
actual = @client.export(params)
|
57
|
+
|
58
|
+
assert_equal(dummy_responses, actual.to_a)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_failure
|
62
|
+
stub_response(failure_response)
|
63
|
+
|
64
|
+
stub(Embulk.logger).error(failure_response.body) {}
|
65
|
+
actual = @client.export(params)
|
66
|
+
|
67
|
+
assert_equal([], actual.to_a)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_failure_logging
|
71
|
+
stub_response(failure_response)
|
72
|
+
|
73
|
+
mock(Embulk.logger).error(failure_response.body) {}
|
74
|
+
@client.export(params)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def stub_response(response)
|
80
|
+
stub(@httpclient).get(Client::ENDPOINT_EXPORT, params) do
|
81
|
+
response
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def success_response
|
86
|
+
Struct.new(:code, :body).new(200, jsonl_dummy_responses)
|
87
|
+
end
|
88
|
+
|
89
|
+
def failure_response
|
90
|
+
Struct.new(:code, :body).new(400, "{'error': 'invalid'}")
|
91
|
+
end
|
92
|
+
|
93
|
+
def params
|
94
|
+
{
|
95
|
+
api_key: API_KEY,
|
96
|
+
api_secret: API_SECRET,
|
97
|
+
from_date: "2015-01-01",
|
98
|
+
to_date: "2015-03-02",
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def dummy_responses
|
103
|
+
[
|
104
|
+
{
|
105
|
+
"event" => "event",
|
106
|
+
"properties" => {
|
107
|
+
"foo" => "FOO",
|
108
|
+
"bar" => "2000-01-01 11:11:11",
|
109
|
+
"int" => 42,
|
110
|
+
}
|
111
|
+
},
|
112
|
+
{
|
113
|
+
"event" => "event2",
|
114
|
+
"properties" => {
|
115
|
+
"foo" => "fooooooooo",
|
116
|
+
"bar" => "1988-12-01 12:11:11",
|
117
|
+
"int" => 1,
|
118
|
+
}
|
119
|
+
},
|
120
|
+
]
|
121
|
+
end
|
122
|
+
|
123
|
+
def jsonl_dummy_responses
|
124
|
+
dummy_responses.map{|res| JSON.dump(res)}.join("\n")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require "prepare_embulk"
|
2
|
+
require "embulk/input/mixpanel"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Embulk
|
6
|
+
module Input
|
7
|
+
class MixpanelTest < Test::Unit::TestCase
|
8
|
+
API_KEY = "api_key".freeze
|
9
|
+
API_SECRET = "api_secret".freeze
|
10
|
+
FROM_DATE = "2015-02-22".freeze
|
11
|
+
TO_DATE = "2015-03-02".freeze
|
12
|
+
|
13
|
+
DURATIONS = [
|
14
|
+
{from_date: FROM_DATE, to_date: "2015-02-28"}, # It has 7 days between 2015-02-22 and 2015-02-28
|
15
|
+
{from_date: "2015-03-01", to_date: TO_DATE},
|
16
|
+
]
|
17
|
+
|
18
|
+
def setup
|
19
|
+
setup_client
|
20
|
+
setup_logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_client
|
24
|
+
params = {
|
25
|
+
api_key: API_KEY,
|
26
|
+
event: nil,
|
27
|
+
where: nil,
|
28
|
+
bucket: nil,
|
29
|
+
}
|
30
|
+
|
31
|
+
any_instance_of(MixpanelApi::Client) do |klass|
|
32
|
+
DURATIONS.each do |duration|
|
33
|
+
from_date = duration[:from_date]
|
34
|
+
to_date = duration[:to_date]
|
35
|
+
|
36
|
+
stub(klass).export(params) { records }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_logger
|
42
|
+
stub(Embulk).logger { ::Logger.new(IO::NULL) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_guess
|
46
|
+
expected = {
|
47
|
+
"columns" => [
|
48
|
+
{name: "event", type: :string},
|
49
|
+
{name: "foo", type: :string},
|
50
|
+
{name: "time", type: :long},
|
51
|
+
{name: "int", type: :long},
|
52
|
+
]
|
53
|
+
}
|
54
|
+
|
55
|
+
actual = Mixpanel.guess(embulk_config)
|
56
|
+
assert_equal(expected, actual)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_export_params
|
60
|
+
config_params = [
|
61
|
+
:type, "mixpanel",
|
62
|
+
:api_key, API_KEY,
|
63
|
+
:api_secret, API_SECRET,
|
64
|
+
:from_date, FROM_DATE,
|
65
|
+
:to_date, TO_DATE,
|
66
|
+
:event, ["ViewHoge", "ViewFuga"],
|
67
|
+
:where, 'properties["$os"] == "Windows"',
|
68
|
+
:bucket, "987",
|
69
|
+
]
|
70
|
+
|
71
|
+
config = DataSource[*config_params]
|
72
|
+
|
73
|
+
expected = {
|
74
|
+
api_key: API_KEY,
|
75
|
+
event: "[\"ViewHoge\",\"ViewFuga\"]",
|
76
|
+
where: 'properties["$os"] == "Windows"',
|
77
|
+
bucket: "987",
|
78
|
+
}
|
79
|
+
actual = Mixpanel.export_params(config)
|
80
|
+
|
81
|
+
assert_equal(expected, actual)
|
82
|
+
end
|
83
|
+
|
84
|
+
class RunTest < self
|
85
|
+
def setup
|
86
|
+
super
|
87
|
+
|
88
|
+
@page_builder = Object.new
|
89
|
+
@plugin = Mixpanel.new(task, nil, nil, @page_builder)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_preview
|
93
|
+
stub(@plugin).preview? { true }
|
94
|
+
mock(@page_builder).add(anything).times(records.length)
|
95
|
+
mock(@page_builder).finish
|
96
|
+
|
97
|
+
@plugin.run
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_run
|
101
|
+
stub(@plugin).preview? { false }
|
102
|
+
mock(@page_builder).add(anything).times(records.length * 2)
|
103
|
+
mock(@page_builder).finish
|
104
|
+
|
105
|
+
@plugin.run
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_timezone
|
109
|
+
stub(@plugin).preview? { false }
|
110
|
+
adjusted = record_epoch - timezone_offset_seconds
|
111
|
+
mock(@page_builder).add(["FOO", adjusted]).times(records.length * 2)
|
112
|
+
mock(@page_builder).finish
|
113
|
+
|
114
|
+
@plugin.run
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_invalid_timezone
|
118
|
+
assert_raise(TZInfo::InvalidTimezoneIdentifier) do
|
119
|
+
Mixpanel.new(task.merge(timezone: "Asia/Tokyooooooooo"), nil, nil, @page_builder).run
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def task
|
126
|
+
{
|
127
|
+
api_key: API_KEY,
|
128
|
+
api_secret: API_SECRET,
|
129
|
+
timezone: "Asia/Tokyo",
|
130
|
+
schema: [
|
131
|
+
{"name" => "foo", "type" => "long"},
|
132
|
+
{"name" => "time", "type" => "long"},
|
133
|
+
],
|
134
|
+
dates: (Date.parse(FROM_DATE)..Date.parse(TO_DATE)).to_a,
|
135
|
+
params: Mixpanel.export_params(embulk_config),
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
def timezone_offset_seconds
|
140
|
+
60 * 60 * 9 # Asia/Tokyo
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def records
|
147
|
+
[
|
148
|
+
{
|
149
|
+
"event" => "event",
|
150
|
+
"properties" => {
|
151
|
+
"foo" => "FOO",
|
152
|
+
"time" => record_epoch,
|
153
|
+
"int" => 42,
|
154
|
+
}
|
155
|
+
},
|
156
|
+
] * 30
|
157
|
+
end
|
158
|
+
|
159
|
+
def record_epoch
|
160
|
+
1234567890
|
161
|
+
end
|
162
|
+
|
163
|
+
def embulk_config
|
164
|
+
config = {
|
165
|
+
type: "mixpanel",
|
166
|
+
api_key: API_KEY,
|
167
|
+
api_secret: API_SECRET,
|
168
|
+
from_date: FROM_DATE,
|
169
|
+
to_date: TO_DATE,
|
170
|
+
}
|
171
|
+
DataSource[*config.to_a.flatten(1)]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "embulk/command/embulk_run"
|
2
|
+
|
3
|
+
classpath_dir = Embulk.home("classpath")
|
4
|
+
jars = Dir.entries(classpath_dir).select{|f| f =~ /\.jar$/ }.sort
|
5
|
+
jars.each do |jar|
|
6
|
+
require File.join(classpath_dir, jar)
|
7
|
+
end
|
8
|
+
require "embulk/java/bootstrap"
|
9
|
+
|
10
|
+
require "embulk"
|
data/test/run-test.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
4
|
+
lib_dir = File.join(base_dir, "lib")
|
5
|
+
test_dir = File.join(base_dir, "test")
|
6
|
+
|
7
|
+
require "test-unit"
|
8
|
+
require "test/unit/rr"
|
9
|
+
require "codeclimate-test-reporter"
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(lib_dir)
|
12
|
+
$LOAD_PATH.unshift(test_dir)
|
13
|
+
|
14
|
+
ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "5000"
|
15
|
+
|
16
|
+
CodeClimate::TestReporter.start
|
17
|
+
|
18
|
+
exit Test::Unit::AutoRunner.run(true, test_dir)
|
metadata
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: embulk-input-mixpanel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- yoshihara
|
8
|
+
- uu59
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-07-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
name: httpclient
|
21
|
+
prerelease: false
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
name: tzinfo
|
35
|
+
prerelease: false
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
name: bundler
|
49
|
+
prerelease: false
|
50
|
+
type: :development
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '1.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
name: rake
|
63
|
+
prerelease: false
|
64
|
+
type: :development
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '10.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.6.16
|
76
|
+
- - <
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '1.0'
|
79
|
+
name: embulk
|
80
|
+
prerelease: false
|
81
|
+
type: :development
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 0.6.16
|
87
|
+
- - <
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
name: pry
|
97
|
+
prerelease: false
|
98
|
+
type: :development
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
name: test-unit
|
111
|
+
prerelease: false
|
112
|
+
type: :development
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
name: test-unit-rr
|
125
|
+
prerelease: false
|
126
|
+
type: :development
|
127
|
+
version_requirements: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
name: codeclimate-test-reporter
|
139
|
+
prerelease: false
|
140
|
+
type: :development
|
141
|
+
version_requirements: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
description: Loads records from Mixpanel.
|
147
|
+
email:
|
148
|
+
- h.yoshihara@everyleaf.com
|
149
|
+
- k@uu59.org
|
150
|
+
executables: []
|
151
|
+
extensions: []
|
152
|
+
extra_rdoc_files: []
|
153
|
+
files:
|
154
|
+
- .gitignore
|
155
|
+
- .travis.yml
|
156
|
+
- CHANGELOG.md
|
157
|
+
- Gemfile
|
158
|
+
- LICENSE
|
159
|
+
- README.md
|
160
|
+
- Rakefile
|
161
|
+
- embulk-input-mixpanel.gemspec
|
162
|
+
- gemfiles/embulk-0.6.16
|
163
|
+
- gemfiles/embulk-0.6.17
|
164
|
+
- gemfiles/embulk-latest
|
165
|
+
- lib/embulk/input/mixpanel.rb
|
166
|
+
- lib/embulk/input/mixpanel_api/client.rb
|
167
|
+
- test/embulk/input/mixpanel_api/test_client.rb
|
168
|
+
- test/embulk/input/test_mixpanel.rb
|
169
|
+
- test/prepare_embulk.rb
|
170
|
+
- test/run-test.rb
|
171
|
+
homepage: https://github.com/treasure-data/embulk-input-mixpanel
|
172
|
+
licenses:
|
173
|
+
- Apache2
|
174
|
+
metadata: {}
|
175
|
+
post_install_message:
|
176
|
+
rdoc_options: []
|
177
|
+
require_paths:
|
178
|
+
- lib
|
179
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - '>='
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - '>='
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '0'
|
189
|
+
requirements: []
|
190
|
+
rubyforge_project:
|
191
|
+
rubygems_version: 2.4.6
|
192
|
+
signing_key:
|
193
|
+
specification_version: 4
|
194
|
+
summary: Mixpanel input plugin for Embulk
|
195
|
+
test_files:
|
196
|
+
- test/embulk/input/mixpanel_api/test_client.rb
|
197
|
+
- test/embulk/input/test_mixpanel.rb
|
198
|
+
- test/prepare_embulk.rb
|
199
|
+
- test/run-test.rb
|