twitter-ads 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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