twitter-ads 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +2 -0
  4. data/CONTRIBUTING.md +77 -0
  5. data/LICENSE +22 -0
  6. data/README.md +111 -0
  7. data/Rakefile +86 -0
  8. data/bin/twitter-ads +42 -0
  9. data/lib/twitter-ads.rb +54 -0
  10. data/lib/twitter-ads/account.rb +229 -0
  11. data/lib/twitter-ads/audiences/tailored_audience.rb +177 -0
  12. data/lib/twitter-ads/campaign/app_list.rb +42 -0
  13. data/lib/twitter-ads/campaign/campaign.rb +40 -0
  14. data/lib/twitter-ads/campaign/funding_instrument.rb +33 -0
  15. data/lib/twitter-ads/campaign/line_item.rb +91 -0
  16. data/lib/twitter-ads/campaign/promotable_user.rb +28 -0
  17. data/lib/twitter-ads/campaign/targeting_criteria.rb +77 -0
  18. data/lib/twitter-ads/campaign/tweet.rb +83 -0
  19. data/lib/twitter-ads/client.rb +92 -0
  20. data/lib/twitter-ads/creative/app_download_card.rb +44 -0
  21. data/lib/twitter-ads/creative/image_app_download_card.rb +44 -0
  22. data/lib/twitter-ads/creative/image_conversation_card.rb +44 -0
  23. data/lib/twitter-ads/creative/lead_gen_card.rb +46 -0
  24. data/lib/twitter-ads/creative/promoted_account.rb +38 -0
  25. data/lib/twitter-ads/creative/promoted_tweet.rb +87 -0
  26. data/lib/twitter-ads/creative/video.rb +43 -0
  27. data/lib/twitter-ads/creative/video_app_download_card.rb +47 -0
  28. data/lib/twitter-ads/creative/video_conversation_card.rb +46 -0
  29. data/lib/twitter-ads/creative/website_card.rb +48 -0
  30. data/lib/twitter-ads/cursor.rb +127 -0
  31. data/lib/twitter-ads/enum.rb +135 -0
  32. data/lib/twitter-ads/error.rb +93 -0
  33. data/lib/twitter-ads/http/request.rb +127 -0
  34. data/lib/twitter-ads/http/response.rb +74 -0
  35. data/lib/twitter-ads/http/ton_upload.rb +140 -0
  36. data/lib/twitter-ads/legacy.rb +7 -0
  37. data/lib/twitter-ads/resources/analytics.rb +90 -0
  38. data/lib/twitter-ads/resources/dsl.rb +108 -0
  39. data/lib/twitter-ads/resources/persistence.rb +43 -0
  40. data/lib/twitter-ads/resources/resource.rb +92 -0
  41. data/lib/twitter-ads/targeting/reach_estimate.rb +69 -0
  42. data/lib/twitter-ads/utils.rb +76 -0
  43. data/lib/twitter-ads/version.rb +6 -0
  44. data/spec/fixtures/accounts_all.json +65 -0
  45. data/spec/fixtures/accounts_features.json +18 -0
  46. data/spec/fixtures/accounts_load.json +19 -0
  47. data/spec/fixtures/app_lists_all.json +22 -0
  48. data/spec/fixtures/app_lists_load.json +31 -0
  49. data/spec/fixtures/campaigns_all.json +208 -0
  50. data/spec/fixtures/campaigns_load.json +27 -0
  51. data/spec/fixtures/funding_instruments_all.json +74 -0
  52. data/spec/fixtures/funding_instruments_load.json +28 -0
  53. data/spec/fixtures/line_items_all.json +292 -0
  54. data/spec/fixtures/line_items_load.json +36 -0
  55. data/spec/fixtures/placements.json +35 -0
  56. data/spec/fixtures/promotable_users_all.json +57 -0
  57. data/spec/fixtures/promotable_users_load.json +18 -0
  58. data/spec/fixtures/promoted_tweets_all.json +212 -0
  59. data/spec/fixtures/promoted_tweets_load.json +19 -0
  60. data/spec/fixtures/reach_estimate.json +19 -0
  61. data/spec/fixtures/tailored_audiences_all.json +67 -0
  62. data/spec/fixtures/tailored_audiences_load.json +29 -0
  63. data/spec/fixtures/tweet_preview.json +24 -0
  64. data/spec/fixtures/videos_all.json +50 -0
  65. data/spec/fixtures/videos_load.json +22 -0
  66. data/spec/quality_spec.rb +15 -0
  67. data/spec/shared/properties.rb +20 -0
  68. data/spec/spec_helper.rb +61 -0
  69. data/spec/support/helpers.rb +42 -0
  70. data/spec/twitter-ads/account_spec.rb +315 -0
  71. data/spec/twitter-ads/audiences/tailored_audience_spec.rb +45 -0
  72. data/spec/twitter-ads/campaign/app_list_spec.rb +108 -0
  73. data/spec/twitter-ads/campaign/line_item_spec.rb +95 -0
  74. data/spec/twitter-ads/campaign/reach_estimate_spec.rb +98 -0
  75. data/spec/twitter-ads/campaign/targeting_criteria_spec.rb +39 -0
  76. data/spec/twitter-ads/campaign/tweet_spec.rb +83 -0
  77. data/spec/twitter-ads/client_spec.rb +115 -0
  78. data/spec/twitter-ads/creative/app_download_card_spec.rb +44 -0
  79. data/spec/twitter-ads/creative/image_app_download_card_spec.rb +43 -0
  80. data/spec/twitter-ads/creative/image_conversation_card_spec.rb +40 -0
  81. data/spec/twitter-ads/creative/lead_gen_card_spec.rb +46 -0
  82. data/spec/twitter-ads/creative/promoted_account_spec.rb +30 -0
  83. data/spec/twitter-ads/creative/promoted_tweet_spec.rb +46 -0
  84. data/spec/twitter-ads/creative/video_app_download_card_spec.rb +42 -0
  85. data/spec/twitter-ads/creative/video_conversation_card_spec.rb +52 -0
  86. data/spec/twitter-ads/creative/video_legacy_spec.rb +43 -0
  87. data/spec/twitter-ads/creative/video_spec.rb +43 -0
  88. data/spec/twitter-ads/creative/website_card_spec.rb +37 -0
  89. data/spec/twitter-ads/cursor_spec.rb +67 -0
  90. data/spec/twitter-ads/placements_spec.rb +36 -0
  91. data/spec/twitter-ads/utils_spec.rb +101 -0
  92. data/twitter-ads.gemspec +37 -0
  93. metadata +247 -0
  94. metadata.gz.sig +0 -0
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+
6
+ class Error < StandardError
7
+
8
+ attr_reader :code, :headers, :response, :details
9
+
10
+ def initialize(*args)
11
+ if args.size == 1 && args[0].respond_to?(:body) && args[0].respond_to?(:code)
12
+ @response = args[0]
13
+ @code = args[0].code
14
+ @details = args[0].body[:errors] if args[0].body.is_a?(Hash) && args[0].body[:errors]
15
+ elsif args.size == 3
16
+ @response = args[0]
17
+ @details = args[1]
18
+ @code = args[2]
19
+ end
20
+ self
21
+ end
22
+
23
+ def inspect
24
+ str = String.new("#<#{self.class.name}:0x#{object_id}")
25
+ str << " code=#{@code}" if @code
26
+ str << " details=\"#{@details}\"" if @details
27
+ str << '>'
28
+ end
29
+ alias to_s inspect
30
+
31
+ class << self
32
+
33
+ ERRORS = {
34
+ 400 => 'TwitterAds::BadRequest',
35
+ 401 => 'TwitterAds::NotAuthorized',
36
+ 403 => 'TwitterAds::Forbidden',
37
+ 404 => 'TwitterAds::NotFound',
38
+ 429 => 'TwitterAds::RateLimit',
39
+ 500 => 'TwitterAds::ServerError',
40
+ 503 => 'TwitterAds::ServiceUnavailable'
41
+ }.freeze
42
+
43
+ # Returns an appropriately typed Error object based from an API response.
44
+ #
45
+ # @param object [Hash] The parsed JSON API response.
46
+ #
47
+ # @return [Error] The error object instance.
48
+ #
49
+ # @since 0.1.0
50
+ # @api private
51
+ def from_response(object)
52
+ return class_eval(ERRORS[object.code]).new(object) if ERRORS.key?(object.code)
53
+ new(object) # fallback, unknown error
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ # Server Errors (5XX)
61
+ class ServerError < Error; end
62
+
63
+ class ServiceUnavailable < ServerError
64
+ attr_reader :retry_after
65
+
66
+ def initialize(object)
67
+ super object
68
+ if object.headers['retry-after']
69
+ @retry_after = object.headers['retry-after']
70
+ end
71
+ self
72
+ end
73
+ end
74
+
75
+ # Client Errors (4XX)
76
+ class ClientError < Error; end
77
+ class NotAuthorized < ClientError; end
78
+ class Forbidden < ClientError; end
79
+ class NotFound < ClientError; end
80
+ class BadRequest < ClientError; end
81
+
82
+ class RateLimit < ClientError
83
+ attr_reader :reset_at, :retry_after
84
+
85
+ def initialize(object)
86
+ super object
87
+ @retry_after = object.headers['retry-after']
88
+ @reset_at = object.headers['rate_limit_reset']
89
+ self
90
+ end
91
+ end
92
+
93
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+
6
+ # Generic container for API requests.
7
+ class Request
8
+
9
+ attr_reader :client, :method, :resource, :options
10
+
11
+ HTTP_METHOD = {
12
+ get: Net::HTTP::Get,
13
+ post: Net::HTTP::Post,
14
+ put: Net::HTTP::Put,
15
+ delete: Net::HTTP::Delete
16
+ }.freeze
17
+
18
+ DEFAULT_DOMAIN = 'https://ads-api.twitter.com'.freeze
19
+ SANDBOX_DOMAIN = 'https://ads-api-sandbox.twitter.com'.freeze
20
+
21
+ private_constant :DEFAULT_DOMAIN, :SANDBOX_DOMAIN, :HTTP_METHOD
22
+
23
+ # Creates a new Request object instance.
24
+ #
25
+ # @example
26
+ # request = Request.new(client, :get, '/0/accounts')
27
+ #
28
+ # @param client [Client] The Client object instance.
29
+ # @param method [Symbol] The HTTP method to be used.
30
+ # @param resource [String] The resource path for the request.
31
+ #
32
+ # @param opts [Hash] An optional Hash of extended options.
33
+ # @option opts [String] :domain Forced override for default domain to use for the request. This
34
+ # value will also override :sandbox mode on the client.
35
+ #
36
+ # @since 0.1.0
37
+ #
38
+ # @return [Request] The Request object instance.
39
+ def initialize(client, method, resource, opts = {})
40
+ @client = client
41
+ @method = method
42
+ @resource = resource
43
+ @options = opts
44
+ self
45
+ end
46
+
47
+ # Executes the current Request object.
48
+ #
49
+ # @example
50
+ # request = Request.new(client, :get, '/0/accounts')
51
+ # request.perform
52
+ #
53
+ # @since 0.1.0
54
+ #
55
+ # @return [Response] The Response object instance generated by the Request.
56
+ def perform
57
+ handle_error(oauth_request)
58
+ end
59
+
60
+ private
61
+
62
+ def domain
63
+ @domain ||= begin
64
+ @options[:domain] || (@client.options[:sandbox] ? SANDBOX_DOMAIN : DEFAULT_DOMAIN)
65
+ end
66
+ end
67
+
68
+ def oauth_request
69
+ request = http_request
70
+ consumer = OAuth::Consumer.new(@client.consumer_key, @client.consumer_secret, site: domain)
71
+ token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
72
+ request.oauth!(consumer.http, consumer, token)
73
+
74
+ write_log(request) if @client.options[:trace]
75
+ response = consumer.http.request(request)
76
+ write_log(response) if @client.options[:trace]
77
+
78
+ Response.new(response.code, response.each {}, response.body)
79
+ end
80
+
81
+ def http_request
82
+ request_url = @resource
83
+ if @options[:params] && !@options[:params].empty?
84
+ request_url += "?#{URI.encode_www_form(@options[:params])}"
85
+ end
86
+
87
+ request = HTTP_METHOD[@method].new(request_url)
88
+ request.body = @options[:body] if @options[:body]
89
+
90
+ @options[:headers].each { |header, value| request[header] = value } if @options[:headers]
91
+ request['user-agent'] = user_agent
92
+
93
+ request
94
+ end
95
+
96
+ def user_agent
97
+ "twitter-ads version: #{TwitterAds::VERSION} " \
98
+ "platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})"
99
+ end
100
+
101
+ def write_log(object)
102
+ if object.respond_to?(:code)
103
+ @client.logger.info("Status: #{object.code} #{object.message}")
104
+ else
105
+ @client.logger.info("Send: #{object.method} #{domain}#{@resource} #{@options[:params]}")
106
+ end
107
+
108
+ object.each { |header| @client.logger.info("Header: #{header}: #{object[header]}") }
109
+
110
+ # suppresses body content for non-Ads API domains (eg. upload.twitter.com)
111
+ if object.body && !object.body.empty?
112
+ if @domain == SANDBOX_DOMAIN || @domain == DEFAULT_DOMAIN
113
+ @client.logger.info("Body: #{object.body}")
114
+ else
115
+ @client.logger.info('Body: **OMITTED**')
116
+ end
117
+ end
118
+ end
119
+
120
+ def handle_error(response)
121
+ raise TwitterAds::Error.from_response(response) unless response.code < 400
122
+ response
123
+ end
124
+
125
+ end
126
+
127
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+
6
+ # Generic container for API responses.
7
+ class Response
8
+
9
+ attr_reader :code,
10
+ :headers,
11
+ :raw_body,
12
+ :body,
13
+ :rate_limit_remaining,
14
+ :rate_limit_reset
15
+
16
+ # Creates a new Response object instance.
17
+ #
18
+ # @example
19
+ # response = Response.new(code, headers, body)
20
+ #
21
+ # @param code [String] The HTTP status code.
22
+ # @param headers [Hash] A Hash object containing HTTP response headers.
23
+ # @param body [String] The response body.
24
+ #
25
+ # @since 0.1.0
26
+ #
27
+ # @return [Response] The Response object instance.
28
+ def initialize(code, headers, body)
29
+ @code = code.to_i
30
+ @headers = headers
31
+ @raw_body = body
32
+
33
+ # handle non-JSON responses
34
+ begin
35
+ @body = TwitterAds::Utils.symbolize!(MultiJson.load(body))
36
+ rescue MultiJson::ParseError
37
+ @body = raw_body
38
+ end
39
+
40
+ if headers.key?('x-rate-limit-reset')
41
+ @rate_limit = headers['x-rate-limit-limit'].first
42
+ @rate_limit_remaining = headers['x-rate-limit-remaining'].first
43
+ @rate_limit_reset = headers['x-rate-limit-reset'].first.to_i
44
+ elsif headers.key?('x-cost-rate-limit-reset')
45
+ @rate_limit = headers['x-cost-rate-limit-limit'].first
46
+ @rate_limit_remaining = headers['x-cost-rate-limit-remaining'].first
47
+ @rate_limit_reset = Time.at(headers['x-cost-rate-limit-reset'].first.to_i)
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ # Returns an inspection string for the current Response instance.
54
+ #
55
+ # @example
56
+ # response.inspect
57
+ #
58
+ # @since 0.1.0
59
+ #
60
+ # @return [String] The inspection string.
61
+ def inspect
62
+ "#<#{self.class.name}:0x#{object_id} code=\"#{@code}\" error=\"#{error?}\">"
63
+ end
64
+
65
+ # Helper method for determining if the current Response contains an error.
66
+ #
67
+ # @return [Boolean] True or false indicating if this Response contains an error.
68
+ def error?
69
+ @error ||= (@code >= 400 && @code <= 599)
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+
6
+ # Specialized request class for TON API uploads.
7
+ class TONUpload
8
+
9
+ DEFAULT_DOMAIN = 'https://ton.twitter.com'.freeze # @api private
10
+ DEFAULT_RESOURCE = '/1.1/ton/bucket/'.freeze # @api private
11
+ DEFAULT_BUCKET = 'ta_partner'.freeze # @api private
12
+ DEFAULT_EXPIRE = (Time.now + 10 * 24 * 60 * 60).httpdate # @api private
13
+ MIN_FILE_SIZE = 1024 * 1024 * 1 # @api private
14
+
15
+ private_constant :DEFAULT_DOMAIN,
16
+ :DEFAULT_BUCKET,
17
+ :DEFAULT_EXPIRE,
18
+ :DEFAULT_RESOURCE,
19
+ :MIN_FILE_SIZE
20
+
21
+ # Creates a new TONUpload object instance.
22
+ #
23
+ # @example
24
+ # request = TONUpload.new(client, '/path/to/file')
25
+ #
26
+ # @param client [Client] The Client object instance.
27
+ # @param file_path [String] The path to the file to be uploaded.
28
+ #
29
+ # @since 0.3.0
30
+ #
31
+ # @return [TONUpload] The TONUpload request instance.
32
+ def initialize(client, file_path, opts = {})
33
+ @file_path = File.expand_path(file_path)
34
+ unless File.exist?(file_path)
35
+ raise ArgumentError.new("Error! The specified file does not exist. (#{file_path})")
36
+ end
37
+
38
+ @file_size = File.size(@file_path)
39
+
40
+ @client = client
41
+ @bucket = opts.delete(:bucket) || DEFAULT_BUCKET
42
+ @options = opts
43
+ self
44
+ end
45
+
46
+ # Executes the current TONUpload object.
47
+ #
48
+ # @example
49
+ # request = TONUpload.new(client, '/path/to/file')
50
+ # request.perform
51
+ #
52
+ # @since 0.3.0
53
+ #
54
+ # @return [String] The upload location provided by the TON API.
55
+ def perform
56
+ if @file_size < MIN_FILE_SIZE
57
+ resource = "#{DEFAULT_RESOURCE}#{@bucket}"
58
+ response = upload(resource, File.read(@file_path))
59
+ response.headers['location'][0]
60
+ else
61
+ response = init_chunked_upload
62
+ chunk_size = response.headers['x-ton-min-chunk-size'][0].to_i
63
+ location = response.headers['location'][0]
64
+
65
+ File.open(@file_path) do |file|
66
+ bytes_read = 0
67
+ while bytes = file.read(chunk_size)
68
+ bytes_start = bytes_read
69
+ bytes_read += bytes.size
70
+ upload_chunk(location, chunk_size, bytes, bytes_start, bytes_read)
71
+ end
72
+ end
73
+
74
+ location
75
+ end
76
+ end
77
+
78
+ # Returns an inspection string for the current object instance.
79
+ #
80
+ # @since 0.3.0
81
+ #
82
+ # @return [String] The object instance detail.
83
+ def inspect
84
+ "#<#{self.class.name}:0x#{object_id} bucket=\"#{@bucket}\" file=\"#{@file_path}\">"
85
+ end
86
+
87
+ private
88
+
89
+ # performs a single chunk upload
90
+ def upload(resource, bytes)
91
+ headers = {
92
+ 'x-ton-expires' => DEFAULT_EXPIRE,
93
+ 'content-length' => @file_size,
94
+ 'content-type' => content_type
95
+ }
96
+ TwitterAds::Request.new(
97
+ @client, :post, resource, domain: DEFAULT_DOMAIN, headers: headers, body: bytes).perform
98
+ end
99
+
100
+ # initialization for a multi-chunk upload
101
+ def init_chunked_upload
102
+ headers = {
103
+ 'x-ton-content-type' => content_type,
104
+ 'x-ton-content-length' => @file_size,
105
+ 'x-ton-expires' => DEFAULT_EXPIRE,
106
+ 'content-length' => 0,
107
+ 'content-type' => content_type
108
+ }
109
+ resource = "#{DEFAULT_RESOURCE}#{@bucket}?resumable=true"
110
+ TwitterAds::Request.new(
111
+ @client, :post, resource, domain: DEFAULT_DOMAIN, headers: headers).perform
112
+ end
113
+
114
+ # uploads a single chunk of a multi-chunk upload
115
+ def upload_chunk(resource, chunk_size, bytes, bytes_start, bytes_read)
116
+ headers = {
117
+ 'content-type' => content_type,
118
+ 'content-length' => [chunk_size, @file_size - bytes_read].min,
119
+ 'content-range' => "bytes #{bytes_start}-#{bytes_read - 1}/#{@file_size}"
120
+ }
121
+ TwitterAds::Request.new(
122
+ @client, :put, resource, domain: DEFAULT_DOMAIN, headers: headers, body: bytes).perform
123
+ end
124
+
125
+ def content_type
126
+ @content_type ||= begin
127
+ extension = File.extname(@file_path).downcase
128
+ if extension == '.csv'
129
+ 'text/csv'
130
+ elsif extension == '.tsv'
131
+ 'text/tab-separated-values'
132
+ else
133
+ 'text/plain'
134
+ end
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ # legacy namespace support, to be removed in v1.0.0 (next major)
5
+ TwitterAds::Objective = TwitterAds::Enum::Objective
6
+ TwitterAds::Product = TwitterAds::Enum::Product
7
+ TwitterAds::Placement = TwitterAds::Enum::Placement
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2015 Twitter, Inc.
3
+
4
+ module TwitterAds
5
+ module Analytics
6
+
7
+ CLASS_ID_MAP = {
8
+ 'TwitterAds::LineItem' => :line_item_ids,
9
+ 'TwitterAds::OrganicTweet' => :tweet_ids,
10
+ 'TwitterAds::Tweet' => :tweet_ids,
11
+ 'TwitterAds::Creative::PromotedTweet' => :promoted_tweet_ids
12
+ }.freeze # @api private
13
+
14
+ def self.included(klass)
15
+ klass.send :include, InstanceMethods
16
+ klass.extend ClassMethods
17
+ end
18
+
19
+ module InstanceMethods
20
+
21
+ # Pulls a list of metrics for the current object instance.
22
+ #
23
+ # @example
24
+ # metrics = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
25
+ # object.stats(metrics)
26
+ #
27
+ # @param metrics [Array] A collection of valid metrics to be fetched.
28
+ # @param opts [Hash] An optional Hash of extended options.
29
+ # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
30
+ # @option opts [Time] :end_time The end time to use (default: now).
31
+ # @option opts [Symbol] :granularity The granularity to use (default: :hour).
32
+ # @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
33
+ #
34
+ # @return [Array] The collection of stats requested.
35
+ #
36
+ # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
37
+ # @since 0.1.0
38
+ def stats(metrics, opts = {})
39
+ self.class.stats(account, [id], metrics, opts)
40
+ end
41
+
42
+ end
43
+
44
+ module ClassMethods
45
+
46
+ # Pulls a list of metrics for a specified set of object IDs.
47
+ #
48
+ # @example
49
+ # ids = ['7o4em', 'oc9ce', '1c5lji']
50
+ # metrics = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
51
+ # object.stats(account, ids, metrics)
52
+ #
53
+ # @param account [Account] The Account object instance.
54
+ # @param ids [Array] A collection of object IDs being targeted.
55
+ # @param metrics [Array] A collection of valid metrics to be fetched.
56
+ # @param opts [Hash] An optional Hash of extended options.
57
+ # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
58
+ # @option opts [Time] :end_time The end time to use (default: now).
59
+ # @option opts [Symbol] :granularity The granularity to use (default: :hour).
60
+ # @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
61
+ #
62
+ # @return [Array] The collection of stats requested.
63
+ #
64
+ # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
65
+ # @since 0.1.0
66
+ def stats(account, ids, metrics, opts = {})
67
+ # set default metric values
68
+ end_time = opts.fetch(:end_time, Time.now)
69
+ start_time = opts.fetch(:start_time, end_time - 604_800) # 7 days ago
70
+ granularity = opts.fetch(:granularity, :hour)
71
+ segmentation_type = opts.fetch(:segmentation_type, nil)
72
+
73
+ params = {
74
+ metrics: metrics.join(','),
75
+ start_time: TwitterAds::Utils.to_time(start_time, granularity),
76
+ end_time: TwitterAds::Utils.to_time(end_time, granularity),
77
+ granularity: granularity.to_s.upcase
78
+ }
79
+ params[:segmentation_type] = segmentation_type.to_s.upcase if segmentation_type
80
+ params[TwitterAds::Analytics::CLASS_ID_MAP[name]] = ids.join(',')
81
+
82
+ resource = self::RESOURCE_STATS % { account_id: account.id }
83
+ response = Request.new(account.client, :get, resource, params: params).perform
84
+ response.body[:data]
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end